diff --git a/qim3d/io/load.py b/qim3d/io/load.py
index 3df15287e88588928041e401595a05a04579c7ef..2a48536aabb8ddba00560389695d42391d448bae 100644
--- a/qim3d/io/load.py
+++ b/qim3d/io/load.py
@@ -12,16 +12,16 @@ Example:
 
 import difflib
 import os
+import re
+import struct
 from pathlib import Path
 
+import dask.array as da
 import h5py
 import nibabel as nib
 import numpy as np
 import olefile
-import struct
-import re
-import dask.array as da
-from pathlib import Path
+import pydicom
 import tifffile
 from PIL import Image, UnidentifiedImageError
 
@@ -30,6 +30,7 @@ from qim3d.io.logger import log
 from qim3d.utils.internal_tools import sizeof, stringify_path
 from qim3d.utils.system import Memory
 
+
 class DataLoader:
     """Utility class for loading data from different file formats.
 
@@ -437,6 +438,39 @@ class DataLoader:
             return vol, meta_data
         else:
             return vol
+        
+    def load_dicom(self, path):
+        """ Load a DICOM file 
+
+        Args:
+            path (str): Path to file
+        """
+        dcm_data = pydicom.dcmread(path)
+        
+        if self.return_metadata:
+            return dcm_data.pixel_array, dcm_data
+        else:
+            return dcm_data.pixel_array
+    
+    def load_dicom_dir(self, path):
+        """ Load a directory of DICOM files into a numpy 3d array
+
+        Args:
+            path (str): Directory path
+        """ 
+        # loop over all .dcm files in the directory
+        files = [f for f in os.listdir(path) if f.endswith('.dcm')]
+        files.sort()
+        # dicom_list contains the dicom objects with metadata
+        dicom_list = [pydicom.dcmread(os.path.join(path, f)) for f in files]
+        # vol contains the pixel data
+        vol = np.stack([dicom.pixel_array for dicom in dicom_list], axis=0)
+        
+        if self.return_metadata:
+            return vol, dicom_list
+        else:
+            return vol
+        
 
     def load(self, path):
         """
@@ -474,6 +508,8 @@ class DataLoader:
                 return self.load_nifti(path)
             elif path.endswith((".vol",".vgi")):
                 return self.load_vol(path)
+            elif path.endswith((".dcm",".DCM")):
+                return self.load_dicom(path)
             else:
                 try:
                     return self.load_pil(path)
@@ -482,7 +518,11 @@ class DataLoader:
 
         # Load a directory
         elif os.path.isdir(path):
-            return self.load_tiff_stack(path)
+            # load dicom if directory contains dicom files else load tiff stack as default
+            if any([f.endswith('.dcm') for f in os.listdir(path)]):
+                return self.load_dicom_dir(path)
+            else:
+                return self.load_tiff_stack(path)
 
         # Fails
         else:
diff --git a/qim3d/io/save.py b/qim3d/io/save.py
index 04cb72d392ec5ec35a8cfd44fbc40e2285bb2583..64a6a1ca5adfd9efab8b7f8d2cb7ebb4f5b25b0f 100644
--- a/qim3d/io/save.py
+++ b/qim3d/io/save.py
@@ -21,13 +21,17 @@ Example:
     ```
 
 """
+import datetime
 import os
 
 import h5py
 import nibabel as nib
 import numpy as np
 import PIL
+import pydicom
 import tifffile
+from pydicom.dataset import FileDataset, FileMetaDataset
+from pydicom.uid import UID
 
 from qim3d.io.logger import log
 from qim3d.utils.internal_tools import sizeof, stringify_path
@@ -179,6 +183,59 @@ class DataSaver:
         with h5py.File(path, "w") as f:
             f.create_dataset("dataset", data=data, compression="gzip" if self.compression else None)
         
+    def save_dicom(self, path, data):
+        """ Save data to a DICOM file to the given path.
+
+        Args:
+            path (str): The path to save file to
+            data (numpy.ndarray): The data to be saved
+        """
+        # based on https://pydicom.github.io/pydicom/stable/auto_examples/input_output/plot_write_dicom.html
+
+        # Populate required values for file meta information
+        file_meta = FileMetaDataset()
+        file_meta.MediaStorageSOPClassUID = UID('1.2.840.10008.5.1.4.1.1.2')
+        file_meta.MediaStorageSOPInstanceUID = UID("1.2.3")
+        file_meta.ImplementationClassUID = UID("1.2.3.4")
+
+        # Create the FileDataset instance (initially no data elements, but file_meta
+        # supplied)
+        ds = FileDataset(path, {},
+                        file_meta=file_meta, preamble=b"\0" * 128)
+
+        ds.PatientName = "Test^Firstname"
+        ds.PatientID = "123456"
+        ds.StudyInstanceUID = "1.2.3.4.5" 
+        ds.SamplesPerPixel = 1
+        ds.PixelRepresentation = 0
+        ds.BitsStored = 16
+        ds.BitsAllocated = 16
+        ds.PhotometricInterpretation = "MONOCHROME2"
+        ds.Rows = data.shape[1]
+        ds.Columns = data.shape[2]
+        ds.NumberOfFrames = data.shape[0]
+        # Set the transfer syntax
+        ds.is_little_endian = True
+        ds.is_implicit_VR = True
+
+        # Set creation date/time
+        dt = datetime.datetime.now()
+        ds.ContentDate = dt.strftime('%Y%m%d')
+        timeStr = dt.strftime('%H%M%S.%f')  # long format with micro seconds
+        ds.ContentTime = timeStr
+        # Needs to be here because of bug in pydicom
+        ds.file_meta.TransferSyntaxUID = pydicom.uid.ImplicitVRLittleEndian
+
+        # Reshape the data into a 1D array and convert to uint16
+        data_1d = data.ravel().astype(np.uint16)
+        # Convert the data to bytes
+        data_bytes = data_1d.tobytes()
+        # Add the data to the DICOM file
+        ds.PixelData = data_bytes
+
+        ds.save_as(path)
+        
+    
     def save_PIL(self, path, data):
         """ Save data to a PIL file to the given path.
 
@@ -271,6 +328,8 @@ class DataSaver:
                         return self.save_h5(path, data)
                     elif path.endswith((".vol",".vgi")):
                         return self.save_vol(path, data)
+                    elif path.endswith((".dcm",".DCM")):
+                        return self.save_dicom(path, data)
                     elif path.endswith((".jpeg",".jpg", ".png")):
                         return self.save_PIL(path, data)
                     else:
diff --git a/requirements.txt b/requirements.txt
index 3338fefbe0a3dbd1e5e042a7dd9c1e706deb16b7..30211e80501de6ae6e74d878cebaf520b2acdd41 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -10,6 +10,7 @@ Pillow>=10.0.1,
 plotly>=5.14.1,
 scipy>=1.11.2,
 seaborn>=0.12.2,
+pydicom>=2.4.4,
 setuptools>=68.0.0,
 tifffile>=2023.4.12,
 torch>=2.0.1,
diff --git a/setup.py b/setup.py
index 92b4f0ec7e13262cfac416ac9720bbff345f24fc..93087983974f465470edc846fa8ba8d84eb1d037 100644
--- a/setup.py
+++ b/setup.py
@@ -42,6 +42,7 @@ setup(
         "h5py>=3.9.0",
         "localthickness>=0.1.2",
         "matplotlib>=3.8.0",
+        "pydicom>=2.4.4",
         "monai>=1.2.0",
         "numpy>=1.26.0",
         "outputformat>=0.1.3",