diff --git a/generate_all_hyteg_forms.py b/generate_all_hyteg_forms.py
index e59ef72ae8e198132ab6fd19282af32bd3a1ea7b..7f9341b5b1079a54b0ffabe628bf9917ad3da426 100644
--- a/generate_all_hyteg_forms.py
+++ b/generate_all_hyteg_forms.py
@@ -29,7 +29,6 @@ from hog.blending import GeometryMap, IdentityMap, ExternalMap, AnnulusMap
 from hog.element_geometry import (
     TriangleElement,
     TetrahedronElement,
-    EmbeddedTriangle,
     ElementGeometry,
 )
 from hog.function_space import FunctionSpace, LagrangianFunctionSpace, N1E1Space
@@ -463,12 +462,12 @@ form_infos = [
         quad_schemes={3: 3},
         blending=ExternalMap(),
     ),
-    FormInfo("manifold_mass", trial_degree=1, test_degree=1, quad_schemes={3: 1}),
+    FormInfo("manifold_mass", trial_degree=1, test_degree=1, quad_schemes={2: 1}),
     FormInfo(
         "manifold_mass",
         trial_degree=1,
         test_degree=1,
-        quad_schemes={3: 3},
+        quad_schemes={2: 3},
         blending=ExternalMap(),
     ),
 ]
@@ -609,7 +608,7 @@ for blending in [IdentityMap(), ExternalMap()]:
             "manifold_vector_mass",
             trial_degree=2,
             test_degree=2,
-            quad_schemes={3: 3},
+            quad_schemes={2: 3},
             row_dim=3,
             col_dim=3,
             is_implemented=is_implemented_for_vector_to_vector,
@@ -622,7 +621,7 @@ for blending in [IdentityMap(), ExternalMap()]:
             "manifold_normal_penalty",
             trial_degree=2,
             test_degree=2,
-            quad_schemes={3: 3},
+            quad_schemes={2: 3},
             row_dim=3,
             col_dim=3,
             is_implemented=is_implemented_for_vector_to_vector,
@@ -635,7 +634,7 @@ for blending in [IdentityMap(), ExternalMap()]:
             "manifold_epsilon",
             trial_degree=2,
             test_degree=2,
-            quad_schemes={3: 3},
+            quad_schemes={2: 3},
             row_dim=3,
             col_dim=3,
             is_implemented=is_implemented_for_vector_to_vector,
@@ -648,7 +647,7 @@ for blending in [IdentityMap(), ExternalMap()]:
             "manifold_epsilon",
             trial_degree=2,
             test_degree=2,
-            quad_schemes={3: 6},
+            quad_schemes={2: 6},
             row_dim=3,
             col_dim=3,
             is_implemented=is_implemented_for_vector_to_vector,
@@ -661,7 +660,7 @@ for blending in [IdentityMap(), ExternalMap()]:
             "vector_laplace_beltrami",
             trial_degree=2,
             test_degree=2,
-            quad_schemes={3: 3},
+            quad_schemes={2: 3},
             row_dim=3,
             col_dim=3,
             is_implemented=is_implemented_for_vector_to_vector,
@@ -677,7 +676,7 @@ for trial_deg, test_deg, transpose in [(1, 2, True), (2, 1, False)]:
                     "manifold_div",
                     trial_degree=trial_deg,
                     test_degree=test_deg,
-                    quad_schemes={3: 3},
+                    quad_schemes={2: 3},
                     row_dim=1,
                     col_dim=3,
                     is_implemented=is_implemented_for_vector_to_scalar,
@@ -690,7 +689,7 @@ for trial_deg, test_deg, transpose in [(1, 2, True), (2, 1, False)]:
                     "manifold_divt",
                     trial_degree=trial_deg,
                     test_degree=test_deg,
-                    quad_schemes={3: 3},
+                    quad_schemes={2: 3},
                     row_dim=3,
                     col_dim=1,
                     is_implemented=is_implemented_for_scalar_to_vector,
@@ -711,7 +710,7 @@ for trial_deg, test_deg, transpose in [
                     "manifold_vector_div",
                     trial_degree=trial_deg,
                     test_degree=test_deg,
-                    quad_schemes={3: 3},
+                    quad_schemes={2: 3},
                     row_dim=1,
                     col_dim=3,
                     is_implemented=is_implemented_for_vector_to_scalar,
@@ -724,7 +723,7 @@ for trial_deg, test_deg, transpose in [
                     "manifold_vector_divt",
                     trial_degree=trial_deg,
                     test_degree=test_deg,
-                    quad_schemes={3: 3},
+                    quad_schemes={2: 3},
                     row_dim=3,
                     col_dim=1,
                     is_implemented=is_implemented_for_scalar_to_vector,
@@ -1056,7 +1055,7 @@ def main():
         geometries = [TetrahedronElement()]
     elif args.geometry == "embedded_triangle":
         logger.info(f"- selected geometry: embedded triangle")
-        geometries = [EmbeddedTriangle()]
+        geometries = [TriangleElement(space_dimension=3)]
     else:
         logger.info(f"- selected geometries: triangle, tetrahedron")
         geometries = [TriangleElement(), TetrahedronElement()]
diff --git a/generate_all_operators.py b/generate_all_operators.py
index 2f81aeefce7ebb8c46278109dcb12d4b29bb5e3b..dd8efae68b8016c338bdac7f1114981062ae2a97 100644
--- a/generate_all_operators.py
+++ b/generate_all_operators.py
@@ -704,7 +704,6 @@ def generate_elementwise_op(
     operator = HyTeGElementwiseOperator(
         name,
         symbolizer,
-        opts=optimizations,
         kernel_wrapper_types=op_info.kernel_types,
         type_descriptor=type_descriptor,
     )
@@ -727,15 +726,14 @@ def generate_elementwise_op(
             blending=blending,  # type: ignore[call-arg] # kw-args are not supported by Callable
         )
 
-        operator.add_integral(
+        operator.add_volume_integral(
             name="".join(name.split()),
-            dim=geometry.dimensions,
-            geometry=geometry,
-            integration_domain=MacroIntegrationDomain.VOLUME,
+            volume_geometry=geometry,
             quad=quad,
             blending=blending,
             form=form,
             loop_strategy=loop_strategy,
+            optimizations=optimizations,
         )
 
     dir_path = os.path.join(args.output, op_info.name.split("_")[0])
diff --git a/hog/blending.py b/hog/blending.py
index 7a8f884e0924f6882fdf65593e416c38fe0c1bf8..01d5a79c0f0513ec4c00c3783da2afacfbd659ce 100644
--- a/hog/blending.py
+++ b/hog/blending.py
@@ -148,7 +148,7 @@ class AnnulusMap(GeometryMap):
         """Evaluates the Jacobian of the geometry map at the passed point."""
 
         if sp.shape(x) != (2, 1):
-            raise HOGException("Invalid input shape for AnnulusMap.")
+            raise HOGException(f"Invalid input shape {sp.shape(x)} for AnnulusMap.")
 
         radRefVertex = self.radRefVertex
         radRayVertex = self.radRayVertex
diff --git a/hog/code_generation.py b/hog/code_generation.py
index e20e3197b26b988b002ed119d87ff6e4c9a0fd09..afa5aaea38a5a0a1a11b1ec72239c4cb747b9234 100644
--- a/hog/code_generation.py
+++ b/hog/code_generation.py
@@ -160,9 +160,9 @@ def replace_multi_assignments(
         # Actually filling the dict.
         for ma in multi_assignments:
             replacement_symbol = replacement_symbols[ma.output_arg()]
-            multi_assignments_replacement_symbols[
-                ma.unique_identifier
-            ] = replacement_symbol
+            multi_assignments_replacement_symbols[ma.unique_identifier] = (
+                replacement_symbol
+            )
 
     if multi_assignments_replacement_symbols:
         with TimedLogger(
@@ -238,9 +238,15 @@ def jacobi_matrix_assignments(
 
     assignments = []
 
-    jac_aff_symbol = symbolizer.jac_ref_to_affine(geometry.dimensions)
-    jac_aff_inv_symbol = symbolizer.jac_ref_to_affine_inv(geometry.dimensions)
-    jac_aff_det_symbol = symbolizer.abs_det_jac_ref_to_affine()
+    # If the reference space dimension (geometry.dimension) is not equal to the space dimension
+    # the Jacobian of the affine map is not square. Processing its inverse and determinant is
+    # thus not meaningful.
+    is_affine_jac_square = geometry.dimensions == geometry.space_dimension
+
+    jac_aff_symbol = symbolizer.jac_ref_to_affine(geometry)
+    if is_affine_jac_square:
+        jac_aff_inv_symbol = symbolizer.jac_ref_to_affine_inv(geometry)
+        jac_aff_det_symbol = symbolizer.abs_det_jac_ref_to_affine()
 
     free_symbols = element_matrix.free_symbols | {
         free_symbol
@@ -250,27 +256,28 @@ def jacobi_matrix_assignments(
     }
 
     # Steps 1 and 2.
-    jac_affine_inv_in_expr = set(jac_aff_inv_symbol).intersection(free_symbols)
-    abs_det_jac_affine_in_expr = jac_aff_det_symbol in free_symbols
-
-    # Just an early exit. Not strictly required, but might accelerate this process in some cases.
-    if jac_affine_inv_in_expr:
-        jac_aff_inv_expr = jac_aff_symbol.inv()
-        for s_ij, e_ij in zip(jac_aff_inv_symbol, jac_aff_inv_expr):
-            if s_ij in jac_affine_inv_in_expr:
-                assignments.append(SympyAssignment(s_ij, e_ij))
+    if is_affine_jac_square:
+        jac_affine_inv_in_expr = set(jac_aff_inv_symbol).intersection(free_symbols)
+        abs_det_jac_affine_in_expr = jac_aff_det_symbol in free_symbols
+
+        if jac_affine_inv_in_expr:
+            jac_aff_inv_expr = jac_aff_symbol.inv()
+            for s_ij, e_ij in zip(jac_aff_inv_symbol, jac_aff_inv_expr):
+                if s_ij in jac_affine_inv_in_expr:
+                    assignments.append(SympyAssignment(s_ij, e_ij))
 
-    if abs_det_jac_affine_in_expr:
-        assignments.append(
-            SympyAssignment(jac_aff_det_symbol, sp.Abs(jac_aff_symbol.det()))
-        )
+        if abs_det_jac_affine_in_expr:
+            assignments.append(
+                SympyAssignment(jac_aff_det_symbol, sp.Abs(jac_aff_symbol.det()))
+            )
 
-    # Collecting all expressions to parse for step 3.
-    free_symbols |= {free_symbol for a in assignments for free_symbol in a.rhs.atoms()}
+        # Collecting all expressions to parse for step 3.
+        free_symbols |= {
+            free_symbol for a in assignments for free_symbol in a.rhs.atoms()
+        }
 
     jac_affine_in_expr = set(jac_aff_symbol).intersection(free_symbols)
 
-    # Just an early exit. Not strictly required, but might accelerate this process in some cases.
     if jac_affine_in_expr:
         jac_aff_expr = jac_ref_to_affine(geometry, symbolizer, affine_points)
         for s_ij, e_ij in zip(jac_aff_symbol, jac_aff_expr):
@@ -335,7 +342,6 @@ def blending_jacobi_matrix_assignments(
 
         jac_blending_in_expr = set(jac_blend_symbol).intersection(free_symbols)
 
-        # Just an early exit. Not strictly required, but might accelerate this process in some cases.
         if jac_blending_in_expr:
             if isinstance(blending, ExternalMap):
                 HOGException("Not implemented or cannot be?")
@@ -344,7 +350,7 @@ def blending_jacobi_matrix_assignments(
             # Collecting all expressions to parse for step 3.
             spat_coord_subs = {}
             for idx, symbol in enumerate(
-                symbolizer.ref_coords_as_list(geometry.dimensions)
+                symbolizer.ref_coords_as_list(quad_info.geometry.dimensions)
             ):
                 spat_coord_subs[symbol] = point[idx]
             jac_blend_expr_sub = jac_blend_expr.subs(spat_coord_subs)
diff --git a/hog/element_geometry.py b/hog/element_geometry.py
index b69c3e95945c5cdf59ee4e05e46f7d807b9067cd..9fe91fc1b7a7e0f886003a20beec765e4f0862c7 100644
--- a/hog/element_geometry.py
+++ b/hog/element_geometry.py
@@ -14,69 +14,68 @@
 # You should have received a copy of the GNU General Public License
 # along with this program.  If not, see <https://www.gnu.org/licenses/>.
 
+from hog.exception import HOGException
+
 
 class ElementGeometry:
-    def __init__(self, dimensions: int, num_vertices: int):
+    def __init__(self, dimensions: int, num_vertices: int, space_dimension: int):
+
+        if space_dimension < dimensions:
+            raise HOGException(
+                "The space dimension should be larger or equal to the dimension of the geometry."
+            )
+
         self.dimensions = dimensions
         self.num_vertices = num_vertices
+        self.space_dimension = space_dimension
 
     def __str__(self):
-        return f"ElementGeometry(dim: {self.dimensions}, vertices: {self.num_vertices})"
+        return f"ElementGeometry(dim: {self.dimensions}, vertices: {self.num_vertices}), space dim: {self.space_dimension}"
 
     def __repr__(self):
         return str(self)
 
     def __hash__(self):
-        return hash((self.dimensions, self.num_vertices))
+        return hash((self.dimensions, self.num_vertices, self.space_dimension))
 
     def __eq__(self, other):
         if isinstance(other, ElementGeometry):
             return (
                 self.dimensions == other.dimensions
                 and self.num_vertices == other.num_vertices
+                and self.space_dimension == other.space_dimension
             )
         return False
 
 
 class LineElement(ElementGeometry):
-    def __init__(self):
-        super().__init__(1, 2)
+    def __init__(self, space_dimension: int = 1):
+        super().__init__(1, 2, space_dimension=space_dimension)
 
     def __str__(self):
-        return f"line, dim: 1, vertices: 2"
+        return f"line, dim: 1, vertices: 2, spacedim: {self.space_dimension}"
 
     def __repr__(self):
         return str(self)
 
 
 class TriangleElement(ElementGeometry):
-    def __init__(self):
-        super().__init__(2, 3)
-
-    def __str__(self):
-        return f"triangle, dim: 2, vertices: 3"
-
-    def __repr__(self):
-        return str(self)
-
-
-class EmbeddedTriangle(ElementGeometry):
-    def __init__(self):
-        super().__init__(3, 3)
+    def __init__(self, space_dimension: int = 2):
+        super().__init__(2, 3, space_dimension=space_dimension)
 
     def __str__(self):
-        return f"embedded triangle, dim: 3, vertices: 3"
+        return f"triangle, dim: 2, vertices: 3, spacedim: {self.space_dimension}"
 
     def __repr__(self):
         return str(self)
 
 
 class TetrahedronElement(ElementGeometry):
-    def __init__(self):
-        super().__init__(3, 4)
+    def __init__(self, space_dimension: int = 3):
+        super().__init__(3, 4, space_dimension=space_dimension)
 
     def __str__(self):
-        return f"tetrahedron, dim: 3, vertices: 4"
+        return f"tetrahedron, dim: 3, vertices: 4, spacedim: {self.space_dimension}"
 
     def __repr__(self):
         return str(self)
diff --git a/hog/fem_helpers.py b/hog/fem_helpers.py
index a6a525fbb850ea8cdc317ec794c428d6c40c8bbe..3cbb2be5dd2efcc32a1f8e47d0759bdad8f53c09 100644
--- a/hog/fem_helpers.py
+++ b/hog/fem_helpers.py
@@ -28,7 +28,6 @@ from hog.blending import (
 from hog.element_geometry import (
     ElementGeometry,
     TriangleElement,
-    EmbeddedTriangle,
     TetrahedronElement,
     LineElement,
 )
@@ -118,12 +117,10 @@ def trafo_ref_to_affine(
                           vector symbols, useful for example if the trafo of two or more different elements is required
     """
     ref_symbols_vector = symbolizer.ref_coords_as_vector(geometry.dimensions)
-    if isinstance(geometry, EmbeddedTriangle):
-        ref_symbols_vector = symbolizer.ref_coords_as_vector(geometry.dimensions - 1)
 
     if affine_points is None:
         affine_points = symbolizer.affine_vertices_as_vectors(
-            geometry.dimensions, geometry.num_vertices
+            geometry.space_dimension, geometry.num_vertices
         )
     else:
         if len(affine_points) != geometry.num_vertices:
@@ -135,12 +132,12 @@ def trafo_ref_to_affine(
                 affine_points[p][d] - affine_points[0][d]
                 for p in range(1, geometry.num_vertices)
             ]
-            for d in range(geometry.dimensions)
+            for d in range(geometry.space_dimension)
         ]
     )
     trafo = trafo * ref_symbols_vector
     trafo = trafo + sp.Matrix(
-        [[affine_points[0][d]] for d in range(geometry.dimensions)]
+        [[affine_points[0][d]] for d in range(geometry.space_dimension)]
     )
     return trafo
 
@@ -199,8 +196,6 @@ def jac_ref_to_affine(
                           vector symbols, useful for example if the trafo of two or more different elements is required
     """
     ref_symbols_list = symbolizer.ref_coords_as_list(geometry.dimensions)
-    if isinstance(geometry, EmbeddedTriangle):
-        ref_symbols_list = symbolizer.ref_coords_as_list(geometry.dimensions - 1)
 
     trafo = trafo_ref_to_affine(geometry, symbolizer, affine_points=affine_points)
     return trafo.jacobian(ref_symbols_list)
@@ -220,8 +215,8 @@ def trafo_ref_to_physical(
         blending_class: type[MultiAssignment]
         if isinstance(geometry, TriangleElement):
             blending_class = BlendingFTriangle
-        elif isinstance(geometry, EmbeddedTriangle):
-            blending_class = BlendingDFEmbeddedTriangle
+            if geometry.space_dimension == 3:
+                blending_class = BlendingDFEmbeddedTriangle
         elif isinstance(geometry, TetrahedronElement):
             blending_class = BlendingFTetrahedron
         else:
@@ -246,15 +241,15 @@ def jac_affine_to_physical(
     blending_class: type[MultiAssignment]
     if isinstance(geometry, TriangleElement):
         blending_class = BlendingDFTriangle
-    elif isinstance(geometry, EmbeddedTriangle):
-        blending_class = BlendingDFEmbeddedTriangle
+        if geometry.space_dimension == 3:
+            blending_class = BlendingDFEmbeddedTriangle
     elif isinstance(geometry, TetrahedronElement):
         blending_class = BlendingDFTetrahedron
     else:
         raise HOGException("Blending not implemented for the passed element geometry.")
 
     t = trafo_ref_to_affine(geometry, symbolizer)
-    jac = sp.zeros(geometry.dimensions, geometry.dimensions)
+    jac = sp.zeros(geometry.space_dimension, geometry.space_dimension)
     rows, cols = jac.shape
     for row in range(rows):
         for col in range(cols):
diff --git a/hog/forms.py b/hog/forms.py
index 8f316d6f6dfa1b085a763c5c7a805713775d8082..8f7ed9f7be3e15c7ecd6cfb1ca1dbbf3ba89ab6d 100644
--- a/hog/forms.py
+++ b/hog/forms.py
@@ -24,6 +24,7 @@ from hog.element_geometry import ElementGeometry, TriangleElement, TetrahedronEl
 from hog.exception import HOGException
 from hog.fem_helpers import (
     trafo_ref_to_affine,
+    trafo_ref_to_physical,
     jac_ref_to_affine,
     jac_affine_to_physical,
     hessian_ref_to_affine,
@@ -96,7 +97,7 @@ Weak formulation
     with TimedLogger("assembling diffusion matrix", level=logging.DEBUG):
         tabulation = Tabulation(symbolizer)
 
-        jac_affine_inv = symbolizer.jac_ref_to_affine_inv(geometry.dimensions)
+        jac_affine_inv = symbolizer.jac_ref_to_affine_inv(geometry)
         jac_affine_det = symbolizer.abs_det_jac_ref_to_affine()
 
         if isinstance(blending, ExternalMap):
@@ -249,8 +250,8 @@ Weak formulation
     with TimedLogger("assembling div-k-grad matrix", level=logging.DEBUG):
         tabulation = Tabulation(symbolizer)
 
-        jac_affine = symbolizer.jac_ref_to_affine(geometry.dimensions)
-        jac_affine_inv = symbolizer.jac_ref_to_affine_inv(geometry.dimensions)
+        jac_affine = symbolizer.jac_ref_to_affine(geometry)
+        jac_affine_inv = symbolizer.jac_ref_to_affine_inv(geometry)
         jac_affine_det = symbolizer.abs_det_jac_ref_to_affine()
 
         if isinstance(blending, ExternalMap):
@@ -360,8 +361,8 @@ Note: :math:`a(c) = 1/8 + u^2` is currently hard-coded and the form is intended
     with TimedLogger("assembling non-linear diffusion matrix", level=logging.DEBUG):
         tabulation = Tabulation(symbolizer)
 
-        jac_affine = symbolizer.jac_ref_to_affine(geometry.dimensions)
-        jac_affine_inv = symbolizer.jac_ref_to_affine_inv(geometry.dimensions)
+        jac_affine = symbolizer.jac_ref_to_affine(geometry)
+        jac_affine_inv = symbolizer.jac_ref_to_affine_inv(geometry)
         jac_affine_det = symbolizer.abs_det_jac_ref_to_affine()
 
         if isinstance(blending, ExternalMap):
@@ -444,8 +445,8 @@ Note: :math:`a(c) = 1/8 + u^2` is currently hard-coded and the form is intended
     ):
         tabulation = Tabulation(symbolizer)
 
-        jac_affine = symbolizer.jac_ref_to_affine(geometry.dimensions)
-        jac_affine_inv = symbolizer.jac_ref_to_affine_inv(geometry.dimensions)
+        jac_affine = symbolizer.jac_ref_to_affine(geometry)
+        jac_affine_inv = symbolizer.jac_ref_to_affine_inv(geometry)
         jac_affine_det = symbolizer.abs_det_jac_ref_to_affine()
 
         if isinstance(blending, ExternalMap):
@@ -571,8 +572,8 @@ where
     with TimedLogger("assembling epsilon matrix", level=logging.DEBUG):
         tabulation = Tabulation(symbolizer)
 
-        jac_affine = symbolizer.jac_ref_to_affine(geometry.dimensions)
-        jac_affine_inv = symbolizer.jac_ref_to_affine_inv(geometry.dimensions)
+        jac_affine = symbolizer.jac_ref_to_affine(geometry)
+        jac_affine_inv = symbolizer.jac_ref_to_affine_inv(geometry)
         jac_affine_det = symbolizer.abs_det_jac_ref_to_affine()
 
         if isinstance(blending, ExternalMap):
@@ -1011,8 +1012,8 @@ Weak formulation
         f"assembling divergence {'transpose' if transpose else ''} matrix",
         level=logging.DEBUG,
     ):
-        jac_affine = symbolizer.jac_ref_to_affine(geometry.dimensions)
-        jac_affine_inv = symbolizer.jac_ref_to_affine_inv(geometry.dimensions)
+        jac_affine = symbolizer.jac_ref_to_affine(geometry)
+        jac_affine_inv = symbolizer.jac_ref_to_affine_inv(geometry)
         jac_affine_det = symbolizer.abs_det_jac_ref_to_affine()
 
         if isinstance(blending, ExternalMap):
@@ -1122,8 +1123,8 @@ where
     with TimedLogger("assembling full stokes matrix", level=logging.DEBUG):
         tabulation = Tabulation(symbolizer)
 
-        jac_affine = symbolizer.jac_ref_to_affine(geometry.dimensions)
-        jac_affine_inv = symbolizer.jac_ref_to_affine_inv(geometry.dimensions)
+        jac_affine = symbolizer.jac_ref_to_affine(geometry)
+        jac_affine_inv = symbolizer.jac_ref_to_affine_inv(geometry)
         jac_affine_det = symbolizer.abs_det_jac_ref_to_affine()
 
         if isinstance(blending, ExternalMap):
@@ -1353,8 +1354,8 @@ The resulting matrix must be multiplied with a vector of ones to be used as the
     with TimedLogger("assembling shear heating matrix", level=logging.DEBUG):
         tabulation = Tabulation(symbolizer)
 
-        jac_affine = symbolizer.jac_ref_to_affine(geometry.dimensions)
-        jac_affine_inv = symbolizer.jac_ref_to_affine_inv(geometry.dimensions)
+        jac_affine = symbolizer.jac_ref_to_affine(geometry)
+        jac_affine_inv = symbolizer.jac_ref_to_affine_inv(geometry)
         jac_affine_det = symbolizer.abs_det_jac_ref_to_affine()
 
         if isinstance(blending, ExternalMap):
@@ -1388,7 +1389,9 @@ The resulting matrix must be multiplied with a vector of ones to be used as the
                 basis_eval=phi_eval_symbols,
             )
         else:
-            raise HOGException("scalar_space_dependent_coefficient currently not supported in opgen.")
+            raise HOGException(
+                "scalar_space_dependent_coefficient currently not supported in opgen."
+            )
             # mu = scalar_space_dependent_coefficient(
             #     "mu", geometry, symbolizer, blending=blending
             # )
@@ -1433,7 +1436,6 @@ The resulting matrix must be multiplied with a vector of ones to be used as the
                 dof_symbols=dof_symbols_uy,
             )
 
-
             # if geometry.dimensions > 2:
             uz, dof_symbols_uz = fem_function_on_element(
                 velocity_function_space,
@@ -1488,21 +1490,17 @@ The resulting matrix must be multiplied with a vector of ones to be used as the
         for data in it:
             phi = data.trial_shape
             psi = data.test_shape
-            
+
             if blending != IdentityMap():
                 affine_factor = (
                     tabulation.register_factor(
                         "affine_factor_symbol",
                         sp.Matrix([phi * psi * jac_affine_det]),
                     )
-                )[
-                    0
-                ]
+                )[0]
                 form = (
                     mu[0]
-                    * (
-                        double_contraction(tau, grad_u)[0]
-                    )
+                    * (double_contraction(tau, grad_u)[0])
                     * jac_blending_det
                     * affine_factor
                 )
@@ -1510,19 +1508,10 @@ The resulting matrix must be multiplied with a vector of ones to be used as the
                 shear_heating_det_symbol = (
                     tabulation.register_factor(
                         "shear_heating_det_symbol",
-                        (
-                            double_contraction(tau, grad_u)
-                        )
-                        * phi
-                        * psi 
-                        * jac_affine_det,
+                        (double_contraction(tau, grad_u)) * phi * psi * jac_affine_det,
                     )
-                )[
-                    0
-                ]
-                form = (
-                    mu[0] * shear_heating_det_symbol
-                )
+                )[0]
+                form = mu[0] * shear_heating_det_symbol
 
             mat[data.row, data.col] = form
 
@@ -1534,7 +1523,6 @@ The resulting matrix must be multiplied with a vector of ones to be used as the
     )
 
 
-
 def divdiv(
     trial: FunctionSpace,
     test: FunctionSpace,
@@ -1680,8 +1668,8 @@ Weak formulation
     with TimedLogger("assembling second derivative matrix", level=logging.DEBUG):
         tabulation = Tabulation(symbolizer)
 
-        jac_affine = symbolizer.jac_ref_to_affine(geometry.dimensions)
-        jac_affine_inv = symbolizer.jac_ref_to_affine_inv(geometry.dimensions)
+        jac_affine = symbolizer.jac_ref_to_affine(geometry)
+        jac_affine_inv = symbolizer.jac_ref_to_affine_inv(geometry)
         jac_affine_det = symbolizer.abs_det_jac_ref_to_affine()
 
         if isinstance(blending, ExternalMap):
@@ -1695,7 +1683,9 @@ Weak formulation
             jac_blending_det = symbolizer.abs_det_jac_affine_to_blending()
             if not isinstance(blending, IdentityMap):
                 # hessian_blending_map = blending.hessian(affine_coords)
-                hessian_blending_map = symbolizer.hessian_blending_map(geometry.dimensions)
+                hessian_blending_map = symbolizer.hessian_blending_map(
+                    geometry.dimensions
+                )
 
         # jac_blending_det = abs(det(jac_blending))
         # with TimedLogger("inverting blending Jacobian", level=logging.DEBUG):
diff --git a/hog/forms_boundary.py b/hog/forms_boundary.py
new file mode 100644
index 0000000000000000000000000000000000000000..edff32d2c0a481aa46148dd4d50a6cb2e39b6993
--- /dev/null
+++ b/hog/forms_boundary.py
@@ -0,0 +1,104 @@
+# HyTeG Operator Generator
+# Copyright (C) 2024  HyTeG Team
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <https://www.gnu.org/licenses/>.
+
+from dataclasses import dataclass
+import logging
+import sympy as sp
+from typing import List, Optional, Tuple
+
+from hog.ast import Operations, count_operations
+from hog.element_geometry import ElementGeometry, TriangleElement, TetrahedronElement
+from hog.exception import HOGException
+from hog.fem_helpers import (
+    trafo_ref_to_affine,
+    trafo_ref_to_physical,
+    jac_ref_to_affine,
+    jac_affine_to_physical,
+    hessian_ref_to_affine,
+    hessian_affine_to_blending,
+    create_empty_element_matrix,
+    element_matrix_iterator,
+    scalar_space_dependent_coefficient,
+    vector_space_dependent_coefficient,
+    fem_function_on_element,
+    fem_function_gradient_on_element,
+)
+from hog.function_space import FunctionSpace, EnrichedGalerkinFunctionSpace, N1E1Space
+from hog.math_helpers import dot, grad, inv, abs, det, double_contraction, e_vec
+from hog.quadrature import Quadrature, Tabulation
+from hog.symbolizer import Symbolizer
+from hog.logger import TimedLogger, get_logger
+from hog.blending import GeometryMap, ExternalMap, IdentityMap
+from hog.forms import Form
+
+
+def mass_boundary(
+    trial: FunctionSpace,
+    test: FunctionSpace,
+    volume_geometry: ElementGeometry,
+    boundary_geometry: ElementGeometry,
+    symbolizer: Symbolizer,
+    blending: GeometryMap = IdentityMap(),
+) -> Form:
+    docstring = f"""
+Mass operator.
+
+Geometry map: {blending}
+
+Weak formulation
+
+    u: trial function (space: {trial})
+    v: test function  (space: {test})
+
+    ∫ uv ds
+"""
+    if trial != test:
+        raise HOGException(
+            "Trial space must be equal to test space to assemble mass matrix."
+        )
+
+    if boundary_geometry.dimensions != boundary_geometry.space_dimension - 1:
+        raise HOGException(
+            "Since you are integrating over a boundary, the boundary element's space dimension should be larger than "
+            "its dimension."
+        )
+
+    with TimedLogger("assembling mass matrix", level=logging.DEBUG):
+        tabulation = Tabulation(symbolizer)
+
+        jac_affine = symbolizer.jac_ref_to_affine(boundary_geometry)
+        jac_blending = symbolizer.jac_affine_to_blending(volume_geometry.dimensions)
+
+        fundamental_form_det = abs(
+            det(jac_affine.T * jac_blending.T * jac_blending * jac_affine)
+        )
+
+        mat = create_empty_element_matrix(trial, test, volume_geometry)
+        it = element_matrix_iterator(trial, test, volume_geometry)
+
+        with TimedLogger(
+            f"integrating {mat.shape[0] * mat.shape[1]} expressions",
+            level=logging.DEBUG,
+        ):
+            for data in it:
+                phi = data.trial_shape
+                psi = data.test_shape
+
+                form = phi * psi * fundamental_form_det**0.5
+
+                mat[data.row, data.col] = form
+
+    return Form(mat, tabulation, symmetric=True, docstring=docstring)
diff --git a/hog/forms_vectorial.py b/hog/forms_vectorial.py
index a5619145cbe36452bd936a8525be1cf4e5da61b4..237b3619db6ce8e13bba2d0084e8ca581953391c 100644
--- a/hog/forms_vectorial.py
+++ b/hog/forms_vectorial.py
@@ -159,8 +159,8 @@ def mass_n1e1(
         raise HOGException("mass_n1e1 form is only implemented for N1E1.")
 
     with TimedLogger("assembling mass matrix", level=logging.DEBUG):
-        jac_affine = symbolizer.jac_ref_to_affine(geometry.dimensions)
-        jac_affine_inv = symbolizer.jac_ref_to_affine_inv(geometry.dimensions)
+        jac_affine = symbolizer.jac_ref_to_affine(geometry)
+        jac_affine_inv = symbolizer.jac_ref_to_affine_inv(geometry)
         jac_affine_det = symbolizer.abs_det_jac_ref_to_affine()
 
         if isinstance(blending, ExternalMap):
@@ -287,7 +287,7 @@ def curl_curl(
         raise HOGException("curl-curl form is only implemented for N1E1.")
 
     with TimedLogger("assembling curl-curl matrix", level=logging.DEBUG):
-        jac_affine = symbolizer.jac_ref_to_affine(geometry.dimensions)
+        jac_affine = symbolizer.jac_ref_to_affine(geometry)
         jac_affine_det = symbolizer.abs_det_jac_ref_to_affine()
 
         if isinstance(blending, ExternalMap):
@@ -353,8 +353,8 @@ Strong formulation
     with TimedLogger("assembling curl_curl_plus_mass matrix", level=logging.DEBUG):
         tabulation = Tabulation(symbolizer)
 
-        jac_affine = symbolizer.jac_ref_to_affine(geometry.dimensions)
-        jac_affine_inv = symbolizer.jac_ref_to_affine_inv(geometry.dimensions)
+        jac_affine = symbolizer.jac_ref_to_affine(geometry)
+        jac_affine_inv = symbolizer.jac_ref_to_affine_inv(geometry)
         jac_affine_det = symbolizer.abs_det_jac_ref_to_affine()
 
         if isinstance(blending, ExternalMap):
diff --git a/hog/function_space.py b/hog/function_space.py
index 8ee718ed5551fb85631fc5a49066fe16fa974889..3e1b048daa84cb68385b61d24c05eaab31f46144 100644
--- a/hog/function_space.py
+++ b/hog/function_space.py
@@ -21,7 +21,6 @@ import sympy as sp
 from hog.element_geometry import (
     ElementGeometry,
     TriangleElement,
-    EmbeddedTriangle,
     TetrahedronElement,
 )
 from hog.exception import HOGException
@@ -89,8 +88,6 @@ class FunctionSpace(Protocol):
         """
         if domain in ["ref", "reference"]:
             symbols = self.symbolizer.ref_coords_as_list(geometry.dimensions)
-            if isinstance(geometry, EmbeddedTriangle):
-                symbols = self.symbolizer.ref_coords_as_list(geometry.dimensions - 1)
             basis_functions_gradients = [
                 grad(f, symbols)
                 for f in self.shape(geometry, domain=domain, dof_map=dof_map)
@@ -201,6 +198,7 @@ class LagrangianFunctionSpace(FunctionSpace):
 
             elif (
                 isinstance(geometry, TriangleElement)
+                and geometry.dimensions == geometry.space_dimension
                 and self.family in ["Lagrange"]
                 and self._degree == 2
             ):
@@ -216,14 +214,16 @@ class LagrangianFunctionSpace(FunctionSpace):
                 ]
 
             elif (
-                isinstance(geometry, EmbeddedTriangle)
+                isinstance(geometry, TriangleElement)
+                and geometry.dimensions == geometry.space_dimension - 1
                 and self.family in ["Lagrange"]
                 and self._degree == 0
             ):
                 basis_functions = [sp.sympify(1)]
 
             elif (
-                isinstance(geometry, EmbeddedTriangle)
+                isinstance(geometry, TriangleElement)
+                and geometry.dimensions == geometry.space_dimension - 1
                 and self.family in ["Lagrange"]
                 and self._degree == 1
             ):
@@ -234,7 +234,8 @@ class LagrangianFunctionSpace(FunctionSpace):
                 ]
 
             elif (
-                isinstance(geometry, EmbeddedTriangle)
+                isinstance(geometry, TriangleElement)
+                and geometry.dimensions == geometry.space_dimension - 1
                 and self.family in ["Lagrange"]
                 and self._degree == 2
             ):
diff --git a/hog/manifold_forms.py b/hog/manifold_forms.py
index f53fcfed70272186bfd12aa2cc7afb418bc7bb9c..81f92c5b6941735829caab8151b67bbf980ad2c8 100644
--- a/hog/manifold_forms.py
+++ b/hog/manifold_forms.py
@@ -17,8 +17,7 @@
 import logging
 import sympy as sp
 
-from hog.element_geometry import ElementGeometry
-from hog.element_geometry import EmbeddedTriangle
+from hog.element_geometry import ElementGeometry, TriangleElement
 from hog.exception import HOGException
 from hog.fem_helpers import (
     trafo_ref_to_affine,
@@ -65,8 +64,10 @@ Weak formulation
             "Trial space must be equal to test space to assemble laplace beltrami matrix."
         )
 
-    if not isinstance(geometry, EmbeddedTriangle):
-        raise HOGException("Laplace Beltrami only works for embedded triangles")
+    if not (isinstance(geometry, TriangleElement) and geometry.space_dimension == 3):
+        raise HOGException(
+            "Laplace Beltrami only works for triangles embedded in 3D space."
+        )
 
     with TimedLogger("assembling laplace beltrami matrix", level=logging.DEBUG):
         tabulation = Tabulation(symbolizer)
@@ -139,8 +140,10 @@ Weak formulation
             "Trial space must be equal to test space to assemble laplace beltrami matrix."
         )
 
-    if not isinstance(geometry, EmbeddedTriangle):
-        raise HOGException("Manifold forms only work for embedded triangles.")
+    if not (isinstance(geometry, TriangleElement) and geometry.space_dimension == 3):
+        raise HOGException(
+            "Laplace Beltrami only works for triangles embedded in 3D space."
+        )
 
     with TimedLogger("assembling laplace beltrami matrix", level=logging.DEBUG):
         tabulation = Tabulation(symbolizer)
@@ -210,8 +213,12 @@ Weak formulation
     with TimedLogger("assembling manifold vector mass matrix", level=logging.DEBUG):
         tabulation = Tabulation(symbolizer)
 
-        if not isinstance(geometry, EmbeddedTriangle):
-            raise HOGException("Manifold forms only work for embedded triangles.")
+        if not (
+            isinstance(geometry, TriangleElement) and geometry.space_dimension == 3
+        ):
+            raise HOGException(
+                "Laplace Beltrami only works for triangles embedded in 3D space."
+            )
 
         projection = face_projection(geometry, symbolizer, blending=blending)
 
@@ -281,8 +288,12 @@ Weak formulation
     with TimedLogger("assembling manifold normal penalty matrix", level=logging.DEBUG):
         tabulation = Tabulation(symbolizer)
 
-        if not isinstance(geometry, EmbeddedTriangle):
-            raise HOGException("Manifold forms only work for embedded triangles.")
+        if not (
+            isinstance(geometry, TriangleElement) and geometry.space_dimension == 3
+        ):
+            raise HOGException(
+                "Laplace Beltrami only works for triangles embedded in 3D space."
+            )
 
         normal = embedded_normal(geometry, symbolizer, blending)
 
@@ -351,8 +362,12 @@ Weak formulation
     with TimedLogger("assembling manifold div matrix", level=logging.DEBUG):
         tabulation = Tabulation(symbolizer)
 
-        if not isinstance(geometry, EmbeddedTriangle):
-            raise HOGException("Manifold forms only work for embedded triangles.")
+        if not (
+            isinstance(geometry, TriangleElement) and geometry.space_dimension == 3
+        ):
+            raise HOGException(
+                "Laplace Beltrami only works for triangles embedded in 3D space."
+            )
 
         jac_affine = jac_ref_to_affine(geometry, symbolizer)
 
@@ -425,8 +440,12 @@ Weak formulation
             f"WARNING: Manifold vector divergence does NOT compute derivative of matrix P yet. Generated form might not work as intended."
         )
 
-        if not isinstance(geometry, EmbeddedTriangle):
-            raise HOGException("Manifold forms only work for embedded triangles.")
+        if not (
+            isinstance(geometry, TriangleElement) and geometry.space_dimension == 3
+        ):
+            raise HOGException(
+                "Laplace Beltrami only works for triangles embedded in 3D space."
+            )
 
         projection_mat = face_projection(geometry, symbolizer, blending=blending)
 
@@ -509,8 +528,12 @@ Weak formulation
     with TimedLogger("assembling manifold epsilon matrix", level=logging.DEBUG):
         tabulation = Tabulation(symbolizer)
 
-        if not isinstance(geometry, EmbeddedTriangle):
-            raise HOGException("Manifold forms only work for embedded triangles.")
+        if not (
+            isinstance(geometry, TriangleElement) and geometry.space_dimension == 3
+        ):
+            raise HOGException(
+                "Laplace Beltrami only works for triangles embedded in 3D space."
+            )
 
         projection_mat = face_projection(geometry, symbolizer, blending=blending)
 
@@ -601,8 +624,12 @@ Weak formulation
     with TimedLogger("assembling vector laplace beltrami matrix", level=logging.DEBUG):
         tabulation = Tabulation(symbolizer)
 
-        if not isinstance(geometry, EmbeddedTriangle):
-            raise HOGException("Manifold forms only work for embedded triangles.")
+        if not (
+            isinstance(geometry, TriangleElement) and geometry.space_dimension == 3
+        ):
+            raise HOGException(
+                "Laplace Beltrami only works for triangles embedded in 3D space."
+            )
 
         projection_mat = face_projection(geometry, symbolizer, blending=blending)
 
diff --git a/hog/manifold_helpers.py b/hog/manifold_helpers.py
index 4d2bf663ab74c27ceb6f1ed96d9b5914e4a3a855..125e36a957d1b08d2bdf17696da538354ca99084 100644
--- a/hog/manifold_helpers.py
+++ b/hog/manifold_helpers.py
@@ -19,7 +19,7 @@ import sympy as sp
 
 from hog.exception import HOGException
 
-from hog.element_geometry import ElementGeometry, EmbeddedTriangle
+from hog.element_geometry import ElementGeometry, TriangleElement
 from hog.symbolizer import Symbolizer
 from hog.blending import GeometryMap, IdentityMap
 from hog.fem_helpers import jac_affine_to_physical
@@ -33,13 +33,13 @@ def embedded_normal(
 ) -> sp.Matrix:
     """Returns an unoriented unit normal vector for an embedded triangle."""
 
-    if not isinstance(geometry, EmbeddedTriangle):
+    if not (isinstance(geometry, TriangleElement) and geometry.space_dimension == 3):
         raise HOGException(
-            "Embedded normal vectors are only defined for embedded triangles."
+            "Embedded normal vectors are only defined for triangles embedded in 3D space."
         )
 
     vert_points = symbolizer.affine_vertices_as_vectors(
-        geometry.dimensions, geometry.num_vertices
+        geometry.space_dimension, geometry.num_vertices
     )
     span0 = vert_points[1] - vert_points[0]
     span1 = vert_points[2] - vert_points[0]
@@ -67,9 +67,9 @@ def face_projection(
 ) -> sp.Matrix:
     """Returns a projection matrix for an embedded triangle."""
 
-    if not isinstance(geometry, EmbeddedTriangle):
+    if not (isinstance(geometry, TriangleElement) and geometry.space_dimension == 3):
         raise HOGException(
-            "Projection matrices are only defined for embedded triangles."
+            "Projection matrices are only defined for triangles embedded in 3D space."
         )
 
     normal = embedded_normal(geometry, symbolizer, blending=blending)
diff --git a/hog/operator_generation/function_space_impls.py b/hog/operator_generation/function_space_impls.py
index 130539f6040a59defd1ed11b668adcffa775ed5f..8421f9ff013e66a58bdd21d67cd943cd9449bc06 100644
--- a/hog/operator_generation/function_space_impls.py
+++ b/hog/operator_generation/function_space_impls.py
@@ -166,8 +166,15 @@ class FunctionSpaceImpl(ABC):
         element_index: Tuple[int, int, int],
         element_type: Union[FaceType, CellType],
         indexing_info: IndexingInfo,
+        element_vertex_ordering: List[int],
     ) -> List[Field.Access]:
-        """Returns a list of local dof values on the current element."""
+        """
+        Returns a list of local dof values on the current element.
+
+        The element_vertex_ordering is a list that specifies the ordering of the reference vertices.
+        The ordering in which the DoFs are returned depends on this list. The "default" ordering is
+        [0, 1, ..., num_vertices - 1].
+        """
         ...
 
     @abstractmethod
@@ -186,6 +193,7 @@ class FunctionSpaceImpl(ABC):
         geometry: ElementGeometry,
         element_index: Tuple[int, int, int],
         element_type: Union[FaceType, CellType],
+        element_vertex_ordering: List[int],
     ) -> Tuple[CustomCodeNode, sp.MatrixBase]:
         """Returns HyTeG code that computes the basis/DoF transformation and a symbolic expression of the result.
 
@@ -214,6 +222,10 @@ class FunctionSpaceImpl(ABC):
         assembling operators into matrices, these transformations must be
         "baked into" the matrix because vectors are assembled locally and our
         communication routine is not performed during the operator application.
+
+        The element_vertex_ordering is a list that specifies the ordering of the reference vertices.
+        The ordering in which the DoFs are returned depends on this list. The "default" ordering is
+        [0, 1, ..., num_vertices - 1].
         """
         ...
 
@@ -296,10 +308,14 @@ class P1FunctionSpaceImpl(FunctionSpaceImpl):
         element_index: Tuple[int, int, int],
         element_type: Union[FaceType, CellType],
         indexing_info: IndexingInfo,
+        element_vertex_ordering: List[int],
     ) -> List[Field.Access]:
         vertex_dof_indices = micro_element_to_vertex_indices(
             geometry, element_type, element_index
         )
+
+        vertex_dof_indices = [vertex_dof_indices[i] for i in element_vertex_ordering]
+
         vertex_array_indices = [
             dof_idx.array_index(geometry, indexing_info)
             for dof_idx in vertex_dof_indices
@@ -320,6 +336,7 @@ class P1FunctionSpaceImpl(FunctionSpaceImpl):
         geometry: ElementGeometry,
         element_index: Tuple[int, int, int],
         element_type: Union[FaceType, CellType],
+        element_vertex_ordering: List[int],
     ) -> Tuple[CustomCodeNode, sp.MatrixBase]:
         return (
             CustomCodeNode("", [], []),
@@ -422,10 +439,14 @@ class P2FunctionSpaceImpl(FunctionSpaceImpl):
         element_index: Tuple[int, int, int],
         element_type: Union[FaceType, CellType],
         indexing_info: IndexingInfo,
+        element_vertex_ordering: List[int],
     ) -> List[Field.Access]:
         vertex_dof_indices = micro_element_to_vertex_indices(
             geometry, element_type, element_index
         )
+
+        vertex_dof_indices = [vertex_dof_indices[i] for i in element_vertex_ordering]
+
         edge_dof_indices = micro_vertex_to_edge_indices(geometry, vertex_dof_indices)
 
         vrtx_array_idcs = [
@@ -454,6 +475,7 @@ class P2FunctionSpaceImpl(FunctionSpaceImpl):
         geometry: ElementGeometry,
         element_index: Tuple[int, int, int],
         element_type: Union[FaceType, CellType],
+        element_vertex_ordering: List[int],
     ) -> Tuple[CustomCodeNode, sp.MatrixBase]:
         return (
             CustomCodeNode("", [], []),
@@ -557,6 +579,7 @@ class P1VectorFunctionSpaceImpl(FunctionSpaceImpl):
         element_index: Tuple[int, int, int],
         element_type: Union[FaceType, CellType],
         indexing_info: IndexingInfo,
+        element_vertex_ordering: List[int],
     ) -> List[Field.Access]:
         """
         Returns the element-local DoFs (field accesses) in a list (i.e., linearized).
@@ -606,6 +629,9 @@ class P1VectorFunctionSpaceImpl(FunctionSpaceImpl):
         vertex_dof_indices = micro_element_to_vertex_indices(
             geometry, element_type, element_index
         )
+
+        vertex_dof_indices = [vertex_dof_indices[i] for i in element_vertex_ordering]
+
         vertex_array_indices = [
             dof_idx.array_index(geometry, indexing_info)
             for dof_idx in vertex_dof_indices
@@ -628,6 +654,7 @@ class P1VectorFunctionSpaceImpl(FunctionSpaceImpl):
         geometry: ElementGeometry,
         element_index: Tuple[int, int, int],
         element_type: Union[FaceType, CellType],
+        element_vertex_ordering: List[int],
     ) -> Tuple[CustomCodeNode, sp.MatrixBase]:
         return (
             CustomCodeNode("", [], []),
@@ -756,6 +783,7 @@ class P2VectorFunctionSpaceImpl(FunctionSpaceImpl):
         element_index: Tuple[int, int, int],
         element_type: Union[FaceType, CellType],
         indexing_info: IndexingInfo,
+        element_vertex_ordering: List[int],
     ) -> List[Field.Access]:
         """
         Returns the element-local DoFs (field accesses) in a list (i.e., linearized).
@@ -772,6 +800,8 @@ class P2VectorFunctionSpaceImpl(FunctionSpaceImpl):
             geometry, element_type, element_index
         )
 
+        vertex_dof_indices = [vertex_dof_indices[i] for i in element_vertex_ordering]
+
         edge_dof_indices = micro_vertex_to_edge_indices(geometry, vertex_dof_indices)
 
         vertex_array_indices = [
@@ -801,14 +831,17 @@ class P2VectorFunctionSpaceImpl(FunctionSpaceImpl):
         return f"P2VectorFunction< {self.type_descriptor.pystencils_type} >"
 
     def includes(self) -> Set[str]:
-        return {f"hyteg/p2functionspace/P2VectorFunction.hpp",
-                f"hyteg/p2functionspace/P2Function.hpp"}
+        return {
+            f"hyteg/p2functionspace/P2VectorFunction.hpp",
+            f"hyteg/p2functionspace/P2Function.hpp",
+        }
 
     def dof_transformation(
         self,
         geometry: ElementGeometry,
         element_index: Tuple[int, int, int],
         element_type: Union[FaceType, CellType],
+        element_vertex_ordering: List[int],
     ) -> Tuple[CustomCodeNode, sp.MatrixBase]:
         return (
             CustomCodeNode("", [], []),
@@ -872,10 +905,14 @@ class N1E1FunctionSpaceImpl(FunctionSpaceImpl):
         element_index: Tuple[int, int, int],
         element_type: Union[FaceType, CellType],
         indexing_info: IndexingInfo,
+        element_vertex_ordering: List[int],
     ) -> List[Field.Access]:
         vertex_dof_indices = micro_element_to_vertex_indices(
             geometry, element_type, element_index
         )
+
+        vertex_dof_indices = [vertex_dof_indices[i] for i in element_vertex_ordering]
+
         edge_dof_indices = micro_vertex_to_edge_indices(geometry, vertex_dof_indices)
         edge_array_indices = [
             dof_idx.array_index(geometry, indexing_info) for dof_idx in edge_dof_indices
@@ -897,7 +934,14 @@ class N1E1FunctionSpaceImpl(FunctionSpaceImpl):
         geometry: ElementGeometry,
         element_index: Tuple[int, int, int],
         element_type: Union[FaceType, CellType],
+        element_vertex_ordering: List[int],
     ) -> Tuple[CustomCodeNode, sp.MatrixBase]:
+
+        if element_vertex_ordering != [0, 1, 2, 3]:
+            raise HOGException(
+                "Element vertex re-ordering not supported for Nédélec elements (yet)."
+            )
+
         Macro = {2: "Face", 3: "Cell"}[geometry.dimensions]
         macro = {2: "face", 3: "cell"}[geometry.dimensions]
         name = "basisTransformation"
diff --git a/hog/operator_generation/indexing.py b/hog/operator_generation/indexing.py
index dd445364aec0157b14a58ede2d6f61af509fd749..b29e566c5e6459c9f4170b1c11edc2ce4cf51d65 100644
--- a/hog/operator_generation/indexing.py
+++ b/hog/operator_generation/indexing.py
@@ -334,7 +334,7 @@ class IndexingInfo:
         )
 
 
-def all_element_types(dimensions: int) -> Union[List[FaceType], List[CellType]]:
+def all_element_types(dimensions: int) -> List[Union[FaceType, CellType]]:
     if dimensions == 2:
         return [FaceType.GRAY, FaceType.BLUE]
     if dimensions == 3:
diff --git a/hog/operator_generation/kernel_types.py b/hog/operator_generation/kernel_types.py
index 9050623c9d65c5a87c88ce944bf9e01fbc615d87..474a9b45d55a797fcd62f2469755a02e92dcee13 100644
--- a/hog/operator_generation/kernel_types.py
+++ b/hog/operator_generation/kernel_types.py
@@ -114,6 +114,7 @@ class KernelType(ABC):
         element_type: Union[FaceType, CellType],
         src_vecs_accesses: List[List[Field.Access]],
         dst_vecs_accesses: List[List[Field.Access]],
+        element_vertex_ordering: List[int],
     ) -> List[ps.astnodes.Node]:
         """
         Operations to be executed after the access resolution and common subexpression elimination on the symbols
@@ -158,6 +159,7 @@ class Apply(KernelType):
         element_type: Union[FaceType, CellType],
         src_vecs_accesses: List[List[Field.Access]],
         dst_vecs_accesses: List[List[Field.Access]],
+        element_vertex_ordering: List[int],
     ) -> List[ps.astnodes.Node]:
         tmp_symbols = sp.numbered_symbols(self.result_prefix)
 
@@ -197,6 +199,7 @@ class AssembleDiagonal(KernelType):
         element_type: Union[FaceType, CellType],
         src_vecs_accesses: List[List[Field.Access]],
         dst_vecs_accesses: List[List[Field.Access]],
+        element_vertex_ordering: List[int],
     ) -> List[ps.astnodes.Node]:
         tmp_symbols = sp.numbered_symbols(self.result_prefix)
 
@@ -243,6 +246,7 @@ class Assemble(KernelType):
         element_type: Union[FaceType, CellType],
         src_vecs_accesses: List[List[Field.Access]],
         dst_vecs_accesses: List[List[Field.Access]],
+        element_vertex_ordering: List[int],
     ) -> List[ps.astnodes.Node]:
         src, dst = dst_vecs_accesses
         el_mat = sp.Matrix(
@@ -254,10 +258,10 @@ class Assemble(KernelType):
 
         # apply basis/dof transformations
         transform_src_code, transform_src_mat = self.src.dof_transformation(
-            geometry, element_index, element_type
+            geometry, element_index, element_type, element_vertex_ordering
         )
         transform_dst_code, transform_dst_mat = self.dst.dof_transformation(
-            geometry, element_index, element_type
+            geometry, element_index, element_type, element_vertex_ordering
         )
         transformed_el_mat = transform_dst_mat.T * el_mat * transform_src_mat
 
diff --git a/hog/operator_generation/loop_strategies.py b/hog/operator_generation/loop_strategies.py
index 9eb28a48cd4277af001d5de9052753a80c3ddcf8..0c370e42b7901a15b0fea556bf75365f608078cf 100644
--- a/hog/operator_generation/loop_strategies.py
+++ b/hog/operator_generation/loop_strategies.py
@@ -16,7 +16,7 @@
 
 from abc import ABC, abstractmethod
 import re
-from typing import Type, Union
+from typing import Type, Union, Dict
 
 from pystencils import TypedSymbol
 from pystencils.astnodes import (
@@ -24,6 +24,7 @@ from pystencils.astnodes import (
     Conditional,
     ResolvedFieldAccess,
     SourceCodeComment,
+    LoopOverCoordinate,
 )
 from pystencils.sympyextensions import fast_subs
 from pystencils.typing import FieldPointerSymbol
@@ -33,6 +34,7 @@ import sympy as sp
 from hog.exception import HOGException
 from hog.operator_generation.pystencils_extensions import (
     loop_over_simplex,
+    loop_over_simplex_facet,
     get_innermost_loop,
     create_field_access,
     create_micro_element_loops,
@@ -96,12 +98,14 @@ class LoopStrategy(ABC):
 
 
 class CUBES(LoopStrategy):
-    """For the "cubes" loop strategy, we want to update all micro-elements in the current "cube", i.e. loop over all
-    # element types for the current micro-element index before incrementing.
-    # This way we only have one loop, but need to insert conditionals that take care of those element types that
-    # are not existing at the loop boundary.
-    # The hope is that this strategy induces better cache locality, and that the conditionals can be automatically
-    # transformed by the loop cutting features of pystencils."""
+    """
+    For the "cubes" loop strategy, we want to update all micro-elements in the current "cube", i.e. loop over all
+    element types for the current micro-element index before incrementing.
+    This way we only have one loop, but need to insert conditionals that take care of those element types that
+    are not existing at the loop boundary.
+    The hope is that this strategy induces better cache locality, and that the conditionals can be automatically
+    transformed by the loop cutting features of pystencils.
+    """
 
     def __init__(self):
         super(CUBES, self).__init__()
@@ -310,3 +314,120 @@ class FUSEDROWS(LoopStrategy):
 
     def __str__(self):
         return "FUSEDROWS"
+
+
+class BOUNDARY(LoopStrategy):
+    """
+    Special loop strategy that only loops over elements of a specified boundary.
+
+    Concretely, this means that it iterates over all elements with facets (edges in 2D, faces in 3D) that fully overlap
+    with the specified boundary (one of 3 macro-edges in 2D, one of 4 macro-faces in 3D).
+
+    To loop over multiple boundaries, just construct multiple loops.
+
+    The loop is constructed using conditionals, see the CUBES loop strategy.
+    """
+
+    def __init__(self, facet_id: int):
+        """
+        Constructs and initializes the BOUNDARY loop strategy.
+
+        :param facet_id: in [0, 2] for 2D, in [0, 3] for 3D
+        """
+        super(BOUNDARY, self).__init__()
+        self.facet_id = facet_id
+        self.element_loops: Dict[Union[FaceType, CellType], LoopOverCoordinate] = dict()
+
+    def create_loop(self, dim, element_index, micro_edges_per_macro_edge):
+
+        if dim == 2:
+            if self.facet_id not in [0, 1, 2]:
+                raise HOGException("Invalid facet ID for BOUNDARY loop strategy in 2D.")
+
+            self.element_loops = {
+                FaceType.GRAY: loop_over_simplex_facet(
+                    dim, micro_edges_per_macro_edge, self.facet_id
+                ),
+            }
+
+        elif dim == 3:
+            if self.facet_id not in [0, 1, 2, 3]:
+                raise HOGException("Invalid facet ID for BOUNDARY loop strategy in 3D.")
+
+            second_cell_type = {
+                0: CellType.BLUE_UP,
+                1: CellType.GREEN_UP,
+                2: CellType.BLUE_DOWN,
+                3: CellType.GREEN_DOWN,
+            }
+
+            self.element_loops = {
+                CellType.WHITE_UP: loop_over_simplex_facet(
+                    dim, micro_edges_per_macro_edge, self.facet_id
+                ),
+                second_cell_type[self.facet_id]: loop_over_simplex_facet(
+                    dim, micro_edges_per_macro_edge - 1, self.facet_id
+                ),
+            }
+
+        return Block(
+            [
+                Block([SourceCodeComment(str(element_type)), loop])
+                for element_type, loop in self.element_loops.items()
+            ]
+        )
+
+    def add_body_to_loop(self, loop, body, element_type):
+        """Register a given loop body to innermost loop of the outer loop corresponding to element_type"""
+        if element_type not in self.element_loops:
+            return
+        innermost_loop = get_innermost_loop(self.element_loops[element_type])
+        body = Block(body)
+        innermost_loop[0].body = body
+        body.parent = innermost_loop[0]
+
+    def get_inner_bodies(self, loop_body):
+        return [
+            inner_loop.body
+            for inner_loop in get_innermost_loop(loop_body, return_all_inner=True)
+        ]
+
+    def add_preloop_for_loop(self, loops, preloop_stmts, element_type):
+        """add given list of statements directly in front of the loop corresponding to element_type."""
+
+        if element_type not in self.element_loops:
+            return loops
+
+        preloop_stmts_lhs_subs = {
+            stmt.lhs: get_element_replacement(stmt.lhs, element_type)
+            for stmt in preloop_stmts
+        }
+
+        if not isinstance(loops, Block):
+            loops = Block(loops)
+
+        blocks = loops.take_child_nodes()
+        new_blocks = []
+        for block in blocks:
+            idx_to_slice_at = -1
+            for idx, stmt in enumerate(block.args):
+                if stmt == self.element_loops[element_type]:
+                    idx_to_slice_at = idx
+                    break
+
+            if idx_to_slice_at == -1:
+                new_blocks.append(block)
+            else:
+                new_stmts = (
+                    block.args[0:idx_to_slice_at]
+                    + preloop_stmts
+                    + block.args[idx_to_slice_at:]
+                )
+                new_block = Block(new_stmts)
+                new_block.fast_subs(preloop_stmts_lhs_subs)
+                new_blocks.append(new_block)
+
+        return new_blocks
+
+    def __str__(self):
+        return "BOUNDARY"
diff --git a/hog/operator_generation/operators.py b/hog/operator_generation/operators.py
index 2eb16fc268b28e80f33601fe340d2b5fc87eba76..d8fd1167c70af2feb3ab02e54db1104b5eeaf20c 100644
--- a/hog/operator_generation/operators.py
+++ b/hog/operator_generation/operators.py
@@ -14,10 +14,10 @@
 # You should have received a copy of the GNU General Public License
 # along with this program.  If not, see <https://www.gnu.org/licenses/>.
 
-from dataclasses import dataclass
+from dataclasses import dataclass, field
 from enum import auto, Enum
 import logging
-from typing import Dict, List, Optional, Set, Tuple
+from typing import Dict, List, Optional, Set, Tuple, Union
 import os
 from textwrap import indent
 
@@ -76,15 +76,27 @@ from hog.blending import GeometryMap
 import hog.code_generation
 import hog.cse
 from hog.dof_symbol import DoFSymbol
-from hog.element_geometry import ElementGeometry
+from hog.element_geometry import (
+    ElementGeometry,
+    TriangleElement,
+    TetrahedronElement,
+)
 from hog.exception import HOGException
 from hog.operator_generation.indexing import (
     all_element_types,
     element_vertex_coordinates,
     IndexingInfo,
+    FaceType,
+    CellType,
+)
+from hog.operator_generation.kernel_types import KernelWrapperType
+from hog.operator_generation.kernel_types import Assemble, KernelType
+from hog.operator_generation.loop_strategies import (
+    LoopStrategy,
+    SAWTOOTH,
+    BOUNDARY,
+    CUBES,
 )
-from hog.operator_generation.kernel_types import KernelWrapperType, KernelType, Assemble
-from hog.operator_generation.loop_strategies import LoopStrategy, SAWTOOTH, CUBES
 from hog.operator_generation.optimizer import Optimizer, Opts
 from hog.quadrature import QuadLoop, Quadrature
 from hog.symbolizer import Symbolizer
@@ -94,9 +106,43 @@ from hog.operator_generation.types import HOGType
 class MacroIntegrationDomain(Enum):
     """Enum type to specify where to integrate."""
 
-    # Integration over the volume element
+    # Integration over the volume element.
     VOLUME = "Volume"
 
+    # Integration over the boundary of the domain (for forms like ∫ ... d(∂Ω)).
+    #
+    # Note: Having one flag for all boundaries of one "type" is not very flexible.
+    #       At some point there should be additional logic that uses HyTeG's BoundaryUIDs.
+    #       To avoid ambiguity, the operator should (at least if there are boundary integrals) have a
+    #       hyteg::BoundaryCondition constructor parameter, and for each boundary integral one hyteg::BoundaryUID
+    #       parameter. Then the BC is tested by comparing those for each facet.
+    #
+    #       Like so (application pseudocode in HyTeG):
+    #
+    #         // generating an operator with one volume and one free-slip boundary integral
+    #         //   ∫ F dx + ∫ G ds
+    #         // where ds corresponds to integrating over parts of the boundary that are marked with a specific
+    #         // BoundaryUID
+    #
+    #         // see HyTeG's documentation and/or BC tutorial
+    #         BoundaryCondition someBC( ... );
+    #
+    #         // setting up the BCs
+    #         BoundaryUID freeslipBC = someBC.createFreeslipBC( ... );
+    #
+    #         // the generated operator
+    #         MyFancyFreeSlipOperator op( storage,
+    #                                     ...,
+    #                                     someBC,    // this is what will be tested against - no ambiguity because src
+    #                                                // and dst func might have different BCs!
+    #                                     freeslipBC // this is the BCUID that will be used for the boundary integral
+    #                                                // if there are more boundary integrals there are more UIDs in the
+    #                                                // constructor (this BCUID is linked to 'ds' above)
+    #                                     );
+    #
+
+    DOMAIN_BOUNDARY = "Domain boundary"
+
 
 @dataclass
 class IntegrationInfo:
@@ -106,6 +152,7 @@ class IntegrationInfo:
     integration_domain: (
         MacroIntegrationDomain  # entity of geometry to integrate over, e.g. facet
     )
+
     quad: Quadrature  # quadrature over integration domain of geometry, e.g. triangle
     blending: GeometryMap
 
@@ -115,8 +162,11 @@ class IntegrationInfo:
 
     loop_strategy: LoopStrategy
 
+    optimizations: Set[Opts]
+
     name: str = "unknown_integral"
     docstring: str = ""
+    boundary_uid_name: str = ""
 
     def _str_(self):
         return f"Integration Info: {self.name}, {self.geometry}, {self.integration_domain}, mat shape {self.mat.shape}, quad degree {self.quad.degree}, blending {self.blending}"
@@ -144,6 +194,66 @@ class CppClassFiles(Enum):
     HEADER_IMPL_AND_VARIANTS = auto()
 
 
+def micro_vertex_permutation_for_facet(
+    volume_geometry: ElementGeometry,
+    element_type: Union[FaceType, CellType],
+    facet_id: int,
+) -> List[int]:
+    """
+    Provides a re-ordering of the micro-vertices such that the facet of the micro-element that coincides with the
+    macro-facet (which is given by the facet_id parameter) is spanned by the first three returned vertex positions.
+
+    The reordering can then for instance be executed by
+
+    ```python
+        element_vertex_order = shuffle_order_for_element_micro_vertices( ... )
+
+        el_vertex_coordinates = [
+            el_vertex_coordinates[i] for i in element_vertex_order
+        ]
+    ```
+
+    """
+
+    if volume_geometry == TriangleElement():
+
+        if element_type == FaceType.BLUE:
+            return [0, 1, 2]
+
+        shuffle_order_gray = {
+            0: [0, 1, 2],
+            1: [0, 2, 1],
+            2: [1, 2, 0],
+        }
+
+        return shuffle_order_gray[facet_id]
+
+    elif volume_geometry == TetrahedronElement():
+
+        if element_type == CellType.WHITE_DOWN:
+            return [0, 1, 2, 3]
+
+        # All element types but WHITE_UP only overlap with a single macro-facet.
+        # It's a different element type for each facet. WHITE_DOWN is never at the boundary.
+        shuffle_order: Dict[Union[FaceType, CellType], Dict[int, List[int]]] = {
+            CellType.WHITE_UP: {
+                0: [0, 1, 2, 3],
+                1: [0, 1, 3, 2],
+                2: [0, 2, 3, 1],
+                3: [1, 2, 3, 0],
+            },
+            CellType.BLUE_UP: {0: [0, 1, 2, 3]},
+            CellType.GREEN_UP: {1: [0, 2, 3, 1]},
+            CellType.BLUE_DOWN: {2: [0, 1, 3, 2]},
+            CellType.GREEN_DOWN: {3: [1, 2, 3, 0]},
+        }
+
+        return shuffle_order[element_type][facet_id]
+
+    else:
+        raise HOGException("Not implemented.")
+
+
 class HyTeGElementwiseOperator:
     """
     This class handles the code generation of HyTeG-type 'elementwise' operators.
@@ -157,6 +267,8 @@ class HyTeGElementwiseOperator:
         "hyteg/edgedofspace/EdgeDoFMacroCell.hpp",
         "hyteg/primitivestorage/PrimitiveStorage.hpp",
         "hyteg/LikwidWrapper.hpp",
+        "hyteg/boundary/BoundaryConditions.hpp",
+        "hyteg/types/types.hpp",
     }
 
     VAR_NAME_MICRO_EDGES_PER_MACRO_EDGE = "micro_edges_per_macro_edge"
@@ -171,7 +283,6 @@ class HyTeGElementwiseOperator:
         name: str,
         symbolizer: Symbolizer,
         kernel_wrapper_types: List[KernelWrapperType],
-        opts: Set[Opts],
         type_descriptor: HOGType,
     ):
         self.name = name
@@ -181,9 +292,6 @@ class HyTeGElementwiseOperator:
         # Each IntegrationInfo object represents one integral of the weak formulation.
         self.integration_infos: Dict[int, List[IntegrationInfo]] = {}
 
-        self._optimizer = Optimizer(opts)
-        self._optimizer_no_vec = Optimizer(opts - {Opts.VECTORIZE, Opts.VECTORIZE512})
-
         # set the precision in which the operations are to be performed
         self._type_descriptor = type_descriptor
 
@@ -192,31 +300,33 @@ class HyTeGElementwiseOperator:
         # implementations for each kernel, generated at a later stage
         self.operator_methods: List[OperatorMethod] = []
 
-    def add_integral(
+    def _add_integral(
         self,
         name: str,
-        dim: int,
-        geometry: ElementGeometry,
+        volume_geometry: ElementGeometry,
         integration_domain: MacroIntegrationDomain,
         quad: Quadrature,
         blending: GeometryMap,
         form: Form,
         loop_strategy: LoopStrategy,
+        boundary_uid_name: str,
+        optimizations: Set[Opts],
     ) -> None:
         """
-        Use this method to add integrals to the operator.
+        Use this method to add integrals to the operator if you know what you are doing. There are helper methods for
+        adding integrals that are a little simpler to use.
 
         :param name: some name for this integral (no spaces please)
-        :param dim: the dimensionality of the domain, i.e. the volume - it may be that this does not match the geometry
-                    of the element, e.g., when facet integrals are required, note that only one routine per dim is
-                    created
-        :param geometry: geometry that shall be integrated over
+        :param volume_geometry: the volume element (even for boundary integrals pass the element with dim == space_dim)
         :param integration_domain: where to integrate - see MacroIntegrationDomain
-        :param quad: the employed quadrature scheme - should match what has been used to integrate the weak form
+        :param quad: the employed quadrature scheme
         :param blending: the same geometry map that has been passed to the form
         :param form: the integrand
         :param loop_strategy: loop pattern over the refined macro-volume - must somehow be compatible with the
                               integration domain
+        :param boundary_uid_name: string that defines the name of the boundary UID if this is a boundary integral
+                                  the parameter is ignored for volume integrals
+        :param optimizations: optimizations that shall be applied to this integral
         """
 
         if "".join(name.split()) != name:
@@ -224,13 +334,15 @@ class HyTeGElementwiseOperator:
                 "Please give the integral an identifier without white space."
             )
 
-        if dim not in [2, 3]:
-            raise HOGException("Only supporting 2D and 3D. Dim should be in [2, 3]")
+        if volume_geometry.space_dimension in self.integration_infos:
+            if name in [
+                ii.name
+                for ii in self.integration_infos[volume_geometry.space_dimension]
+            ]:
+                raise HOGException(f"Integral with name {name} already added!")
 
-        if integration_domain != MacroIntegrationDomain.VOLUME:
-            raise HOGException("Only volume integrals supported as of now.")
-        if dim != geometry.dimensions:
-            raise HOGException("Only volume integrals supported as of now.")
+        if volume_geometry.space_dimension not in [2, 3]:
+            raise HOGException("Only supporting 2D and 3D. Dim should be in [2, 3]")
 
         if integration_domain == MacroIntegrationDomain.VOLUME and not (
             isinstance(loop_strategy, SAWTOOTH) or isinstance(loop_strategy, CUBES)
@@ -241,14 +353,14 @@ class HyTeGElementwiseOperator:
         quad_loop = None
         mat = form.integrand
 
-        if self._optimizer[Opts.TABULATE]:
+        if Opts.TABULATE in optimizations:
             mat = form.tabulation.resolve_table_accesses(mat, self._type_descriptor)
             with TimedLogger(f"constructing tables", level=logging.DEBUG):
                 tables = form.tabulation.construct_tables(quad, self._type_descriptor)
         else:
             mat = form.tabulation.inline_tables(mat)
 
-        if self._optimizer[Opts.QUADLOOPS]:
+        if Opts.QUADLOOPS in optimizations:
             quad_loop = QuadLoop(
                 self.symbolizer,
                 quad,
@@ -272,13 +384,13 @@ class HyTeGElementwiseOperator:
                                 mat[row, col], self.symbolizer, blending
                             )
 
-        if dim not in self.integration_infos:
-            self.integration_infos[dim] = []
+        if volume_geometry.space_dimension not in self.integration_infos:
+            self.integration_infos[volume_geometry.space_dimension] = []
 
-        self.integration_infos[dim].append(
+        self.integration_infos[volume_geometry.space_dimension].append(
             IntegrationInfo(
                 name=name,
-                geometry=geometry,
+                geometry=volume_geometry,
                 integration_domain=integration_domain,
                 quad=quad,
                 blending=blending,
@@ -287,17 +399,125 @@ class HyTeGElementwiseOperator:
                 mat=mat,
                 docstring=form.docstring,
                 loop_strategy=loop_strategy,
+                boundary_uid_name=boundary_uid_name,
+                optimizations=optimizations,
             )
         )
 
+    def add_volume_integral(
+        self,
+        name: str,
+        volume_geometry: ElementGeometry,
+        quad: Quadrature,
+        blending: GeometryMap,
+        form: Form,
+        loop_strategy: LoopStrategy,
+        optimizations: Union[None, Set[Opts]] = None,
+    ) -> None:
+        """
+        Adds a volume integral to the operator. Wrapper around _add_integral() for volume integrals.
+
+        :param name: some name for this integral (no spaces please)
+        :param volume_geometry: the volume element
+        :param quad: the employed quadrature scheme
+        :param blending: the same geometry map that has been passed to the form
+        :param form: the integrand
+        :param loop_strategy: loop pattern over the refined macro-volume - must somehow be compatible with the
+                              integration domain
+        :param optimizations: optimization applied to this integral
+        """
+        if optimizations is None:
+            optimizations = set()
+
+        if volume_geometry.dimensions != quad.geometry.dimensions:
+            raise HOGException(
+                "The quadrature geometry does not match the volume geometry."
+            )
+
+        self._add_integral(
+            name,
+            volume_geometry,
+            MacroIntegrationDomain.VOLUME,
+            quad,
+            blending,
+            form,
+            loop_strategy,
+            "",
+            optimizations,
+        )
+
+    def add_boundary_integral(
+        self,
+        name: str,
+        volume_geometry: ElementGeometry,
+        quad: Quadrature,
+        blending: GeometryMap,
+        form: Form,
+        optimizations: Union[None, Set[Opts]] = None,
+    ) -> None:
+        """
+        Adds a boundary integral to the operator. Wrapper around _add_integral() for boundary integrals.
+
+        :param name: some name for this integral (no spaces please)
+        :param volume_geometry: the volume element (not the geometry of the boundary, also no embedded elements, just
+                                the volume element geometry)
+        :param quad: the employed quadrature scheme - this must use the embedded geometry (e.g., for boundary integrals
+                     in 2D, this should use a LineElement(space_dimension=2))
+        :param blending: the same map that has been passed to the form
+        :param form: the integrand
+        :param optimizations: optimization applied to this integral
+        """
+
+        if optimizations is None:
+            optimizations = set()
+
+        allowed_boundary_optimizations = {Opts.MOVECONSTANTS}
+        if optimizations - allowed_boundary_optimizations != set():
+            raise HOGException(
+                f"Only allowed (aka tested and working) optimizations for boundary integrals are "
+                f"{allowed_boundary_optimizations}."
+            )
+
+        if volume_geometry not in [TriangleElement(), TetrahedronElement()]:
+            raise HOGException(
+                "Boundary integrals only implemented for triangle and tetrahedral elements."
+            )
+
+        if volume_geometry.dimensions - 1 != quad.geometry.dimensions:
+            raise HOGException(
+                "The quadrature geometry does not match the boundary geometry."
+            )
+
+        # Since we will only integrate over the reference facet that lies on the x-line (2D) or xy-plane (3D) we need to
+        # set the last reference coordinate to zero since it will otherwise appear as a free, uninitialized variable.
+        #
+        # This has to be repeated later before the qudrature is applied in case we are working with symbols.
+        #
+        form.integrand = form.integrand.subs(
+            self.symbolizer.ref_coords_as_list(volume_geometry.dimensions)[-1], 0
+        )
+
+        for facet_id in range(volume_geometry.num_vertices):
+            self._add_integral(
+                name + f"_facet_id_{facet_id}",
+                volume_geometry,
+                MacroIntegrationDomain.DOMAIN_BOUNDARY,
+                quad,
+                blending,
+                form,
+                BOUNDARY(facet_id=facet_id),
+                name + "_boundary_uid",
+                optimizations,
+            )
+
     def coefficients(self) -> List[FunctionSpaceImpl]:
         """Returns all coefficients sorted by name.
 
         During generation coefficents are detected in the element matrix and
         stored in the `coeffs` field. Being a Python dictionary, iterating over
         it yields the coefficients in an arbitrary order. Whenever generating
-        code for all coefficents it is a good idea to access them in a well
-        defined order. Most importantly, the order of constructor arguments must
+        code for all coefficents it is a good idea to access them in a well-defined
+        order. Most importantly, the order of constructor arguments must
         be deterministic.
         """
         return sorted(self.coeffs.values(), key=lambda c: c.name)
@@ -311,17 +531,18 @@ class HyTeGElementwiseOperator:
         """
         Invokes the code generation process, writing the full operator C++ code to file.
 
-        :param dir_path:      directory where to write the files - the file names are built automatically
-        :param loop_strategy: iteration pattern
-        :param header_only:   if True, the entire class (incl. implementation) is written into a single file
-        :clang_format_binary: path and/or name of binary for clang-format, defaults to None, which turns
-                              off formatting
+        :param dir_path:            directory where to write the files - the file names are built automatically
+        :param class_files:         determines whether header and or impl files are generated
+        :param clang_format_binary: path and/or name of binary for clang-format, defaults to None, which turns
+                                    off formatting
         """
 
-        # Asking the optimizer if optimizations are valid.
-        self._optimizer.check_opts_validity()
-        # Generate each kernel type (apply, gemv, ...).
-        self.generate_kernels()
+        with TimedLogger(
+            f"Generating kernels for operator {self.name}", level=logging.INFO
+        ):
+
+            # Generate each kernel type (apply, gemv, ...).
+            self.generate_kernels()
 
         # Setting up the final C++ class.
         operator_cpp_class = CppClass(
@@ -427,6 +648,30 @@ class HyTeGElementwiseOperator:
                         )
                     )
 
+        # Let's now check whether we need ctor arguments and member variables for boundary integrals.
+        boundary_condition_vars = []
+        for integration_infos in self.integration_infos.values():
+            if not all(
+                ii.integration_domain == MacroIntegrationDomain.VOLUME
+                for ii in integration_infos
+            ):
+                bc_var = CppVariable(name="boundaryCondition", type="BoundaryCondition")
+                if bc_var not in boundary_condition_vars:
+                    boundary_condition_vars.append(bc_var)
+
+            for ii in integration_infos:
+                if ii.integration_domain == MacroIntegrationDomain.DOMAIN_BOUNDARY:
+                    bcuid_var = CppVariable(
+                        name=ii.boundary_uid_name, type="BoundaryUID"
+                    )
+                    if bcuid_var not in boundary_condition_vars:
+                        boundary_condition_vars.append(bcuid_var)
+
+        boundary_condition_vars_members = [
+            CppVariable(name=bcv.name + "_", type=bcv.type)
+            for bcv in boundary_condition_vars
+        ]
+
         # Finally we know what fields we need and can build the constructors, member variables, and includes.
 
         # Constructors ...
@@ -450,9 +695,16 @@ class HyTeGElementwiseOperator:
                         is_reference=True,
                     )
                     for coeff in self.coefficients()
-                ],
+                ]
+                + boundary_condition_vars,
                 initializer_list=["Operator( storage, minLevel, maxLevel )"]
-                + [f"{coeff.name}( _{coeff.name} )" for coeff in self.coefficients()],
+                + [f"{coeff.name}( _{coeff.name} )" for coeff in self.coefficients()]
+                + [
+                    f"{bcv[0].name}( {bcv[1].name} )"
+                    for bcv in zip(
+                        boundary_condition_vars_members, boundary_condition_vars
+                    )
+                ],
             )
         )
 
@@ -468,7 +720,11 @@ class HyTeGElementwiseOperator:
                 )
             )
 
-        os.makedirs(dir_path, exist_ok=True)  # Create path if it doesn't exist
+        for bcv in boundary_condition_vars_members:
+            operator_cpp_class.add(CppMemberVariable(bcv, visibility="private"))
+
+        # Create path if it doesn't exist
+        os.makedirs(dir_path, exist_ok=True)
 
         output_path_header = os.path.join(dir_path, f"{self.name}.hpp")
         output_path_impl = os.path.join(dir_path, f"{self.name}.cpp")
@@ -625,7 +881,10 @@ class HyTeGElementwiseOperator:
         # This list is only filled if we want to vectorize.
         loop_counter_custom_code_nodes = []
 
-        if not (self._optimizer[Opts.VECTORIZE] or self._optimizer[Opts.VECTORIZE512]):
+        if (
+            Opts.VECTORIZE not in integration_info.optimizations
+            and Opts.VECTORIZE512 not in integration_info.optimizations
+        ):
             # The Jacobians are loop-counter dependent, and we do not care about vectorization.
             # So we just use the indices. pystencils will handle casting them to float.
             el_matrix_element_index = element_index.copy()
@@ -689,7 +948,9 @@ class HyTeGElementwiseOperator:
             ]
 
             # Let's fill the array.
-            float_ctr_array_size = 8 if self._optimizer[Opts.VECTORIZE512] else 4
+            float_ctr_array_size = (
+                8 if Opts.VECTORIZE512 in integration_info.optimizations else 4
+            )
 
             custom_code = ""
             custom_code += f"const int64_t phantom_ctr_0 = ctr_0;\n"
@@ -756,6 +1017,9 @@ class HyTeGElementwiseOperator:
 
         rows, cols = mat.shape
 
+        optimizer = Optimizer(integration_info.optimizations)
+        optimizer.check_opts_validity()
+
         kernel_config = CreateKernelConfig(
             default_number_float=self._type_descriptor.pystencils_type,
             data_type=self._type_descriptor.pystencils_type,
@@ -794,7 +1058,7 @@ class HyTeGElementwiseOperator:
 
         # Common subexpression elimination.
         with TimedLogger("cse on kernel operation", logging.DEBUG):
-            cse_impl = self._optimizer.cse_impl()
+            cse_impl = optimizer.cse_impl()
             kernel_op_assignments = hog.cse.cse(
                 kernel_op_assignments,
                 cse_impl,
@@ -810,7 +1074,7 @@ class HyTeGElementwiseOperator:
 
             with TimedLogger("constructing quadrature loops"):
                 quad_loop = integration_info.quad_loop.construct_quad_loop(
-                    accessed_mat_entries, self._optimizer.cse_impl()
+                    accessed_mat_entries, optimizer.cse_impl()
                 )
         else:
             quad_loop = []
@@ -861,7 +1125,52 @@ class HyTeGElementwiseOperator:
         # element coordinates, jacobi matrix, tabulations, etc.
         preloop_stmts = {}
 
-        for element_type in all_element_types(geometry.dimensions):
+        # Deciding on which element types we want to iterate over.
+        # We skip certain element types for macro-volume boundary integrals.
+        element_types: List[Union[FaceType, CellType]] = all_element_types(
+            geometry.dimensions
+        )
+        if isinstance(integration_info.loop_strategy, BOUNDARY):
+            element_types = list(integration_info.loop_strategy.element_loops.keys())
+
+        for element_type in element_types:
+
+            # Re-ordering micro-element vertices for the handling of domain boundary integrals.
+            #
+            # Boundary integrals are handled by looping over all (volume-)elements that have a facet at one of the
+            # macro-element boundaries. There are three such boundaries in 2D and four in 3D. For each case, a separate
+            # kernel has to be generated. The logic that decides which of these kernels are called on which
+            # macro-volumes is handled by the HyTeG operator that is generated around these kernels.
+            #
+            # Instead of integrating over the micro-volume, we need to integrate over one of the micro-element facets.
+            # For that, the micro-element vertices are re-ordered such that the transformation from the affine element
+            # to the reference element results in the integration (boundary-)domain being mapped to the reference
+            # domain.
+            #
+            # E.g., in 2D, if the integration over the xy-boundary of the macro-volume (== macro-face) shall be
+            # generated this is signalled here by loop_strategy.facet_id == 2.
+            # In 2D all boundaries are only touched by the GRAY micro-elements (this is a little more complicated in 3D
+            # where at each macro-volume boundary, two types of elements overlap).
+            # Thus, we
+            #   a) Shuffle the affine element vertices such that the xy-boundary of the GRAY elements is mapped to the
+            #      (0, 1) line, which is the reference line for integration.
+            #   b) Only iterate over the GRAY elements (handled later below).
+
+            # Default order.
+            element_vertex_order = list(range(geometry.num_vertices))
+
+            # Shuffling vertices if a boundary integral is requested.
+            if (
+                integration_info.integration_domain
+                == MacroIntegrationDomain.DOMAIN_BOUNDARY
+                and isinstance(integration_info.loop_strategy, BOUNDARY)
+            ):
+                element_vertex_order = micro_vertex_permutation_for_facet(
+                    volume_geometry=geometry,
+                    element_type=element_type,
+                    facet_id=integration_info.loop_strategy.facet_id,
+                )
+
             # Create array accesses to the source and destination vector(s) for
             # the kernel.
             src_vecs_accesses = [
@@ -870,6 +1179,7 @@ class HyTeGElementwiseOperator:
                     element_index,  # type: ignore[arg-type] # list of sympy expressions also works
                     element_type,
                     indexing_info,
+                    element_vertex_order,
                 )
                 for src_field in src_fields
             ]
@@ -879,6 +1189,7 @@ class HyTeGElementwiseOperator:
                     element_index,  # type: ignore[arg-type] # list of sympy expressions also works
                     element_type,
                     indexing_info,
+                    element_vertex_order,
                 )
                 for dst_field in dst_fields
             ]
@@ -896,6 +1207,7 @@ class HyTeGElementwiseOperator:
                 element_type,
                 src_vecs_accesses,
                 dst_vecs_accesses,
+                element_vertex_order,
             )
 
             # Load DoFs of coefficients. Those appear whenever a form is
@@ -906,6 +1218,16 @@ class HyTeGElementwiseOperator:
                 for ass in kernel_op_assignments + quad_loop
                 for a in ass.atoms(DoFSymbol)
             }
+
+            if (
+                integration_info.integration_domain != MacroIntegrationDomain.VOLUME
+                and len(dof_symbols_set) > 0
+            ):
+                raise HOGException(
+                    "Boundary integrals and FE coefficients cannot be combined at the moment."
+                    "Dev note: the coefficients need to be sorted."
+                )
+
             dof_symbols = sorted(dof_symbols_set, key=lambda ds: ds.name)
             coeffs = dict(
                 (
@@ -925,7 +1247,11 @@ class HyTeGElementwiseOperator:
                     SympyAssignment(
                         sp.Symbol(dof_symbol.name),
                         coeffs[dof_symbol.function_id].local_dofs(
-                            geometry, element_index, element_type, indexing_info
+                            geometry,
+                            element_index,
+                            element_type,
+                            indexing_info,
+                            element_vertex_order,
                         )[dof_symbol.dof_id],
                     )
                 )
@@ -953,6 +1279,13 @@ class HyTeGElementwiseOperator:
                 self.symbolizer,
             )
 
+            # Re-ordering the element vertex coordinates
+            # (for all computations but affine transformation Jacobians - those are re-ordered later).
+            # See comment on boundary integrals above.
+            el_vertex_coordinates = [
+                el_vertex_coordinates[i] for i in element_vertex_order
+            ]
+
             coords_assignments = [
                 SympyAssignment(
                     element_vertex_coordinates_symbols[vertex][component],
@@ -962,7 +1295,7 @@ class HyTeGElementwiseOperator:
                 for component in range(geometry.dimensions)
             ]
 
-            if integration_info.blending.is_affine() or self._optimizer[Opts.QUADLOOPS]:
+            if integration_info.blending.is_affine() or optimizer[Opts.QUADLOOPS]:
                 blending_assignments = []
             else:
                 blending_assignments = (
@@ -988,7 +1321,7 @@ class HyTeGElementwiseOperator:
                 )
 
                 with TimedLogger("cse on blending operation", logging.DEBUG):
-                    cse_impl = self._optimizer.cse_impl()
+                    cse_impl = optimizer.cse_impl()
                     blending_assignments = hog.cse.cse(
                         blending_assignments,
                         cse_impl,
@@ -1006,7 +1339,25 @@ class HyTeGElementwiseOperator:
                 + kernel_op_post_assignments
             )
 
-            if not self._optimizer[Opts.QUADLOOPS]:
+            if (
+                integration_info.integration_domain
+                == MacroIntegrationDomain.DOMAIN_BOUNDARY
+                and isinstance(integration_info.loop_strategy, BOUNDARY)
+            ):
+                with TimedLogger(
+                    "boundary integrals: setting unused reference coordinate to 0",
+                    logging.DEBUG,
+                ):
+                    for node in body:
+                        node.subs(
+                            {
+                                self.symbolizer.ref_coords_as_list(geometry.dimensions)[
+                                    -1
+                                ]: 0
+                            }
+                        )
+
+            if not optimizer[Opts.QUADLOOPS]:
                 # Only now we replace the quadrature points and weights - if there are any.
                 # We also setup sympy assignments in body
                 with TimedLogger(
@@ -1060,6 +1411,12 @@ class HyTeGElementwiseOperator:
                     self.symbolizer,
                 )
 
+                # Re-ordering the element vertex coordinates for the Jacobians.
+                # See comment on boundary integrals above.
+                el_vertex_coordinates = [
+                    el_vertex_coordinates[i] for i in element_vertex_order
+                ]
+
                 coords_assignments = [
                     SympyAssignment(
                         coord_symbols_for_jac_affine[vertex][component],
@@ -1072,7 +1429,7 @@ class HyTeGElementwiseOperator:
                 elem_dependent_stmts = (
                     hog.cse.cse(
                         coords_assignments + jacobi_assignments,
-                        self._optimizer.cse_impl(),
+                        optimizer.cse_impl(),
                         "tmp_coords_jac",
                         return_type=SympyAssignment,
                     )
@@ -1141,7 +1498,7 @@ class HyTeGElementwiseOperator:
 
                     # generate AST of kernel loop
                     with TimedLogger(
-                        f"Generating kernel {integration_info.name} (wrapper: {kernel_wrapper_type.name} in {dim}D",
+                        f"Generating kernel {integration_info.name} ({kernel_wrapper_type.name}, {dim}D)",
                         logging.INFO,
                     ):
 
@@ -1174,11 +1531,12 @@ class HyTeGElementwiseOperator:
                         f"Optimizing kernel: {kernel_function.function_name} in {dim}D",
                         logging.INFO,
                     ):
-                        optimizer = (
-                            self._optimizer
-                            if not isinstance(kernel_wrapper_type.kernel_type, Assemble)
-                            else self._optimizer_no_vec
-                        )
+                        optimizer = Optimizer(integration_info.optimizations)
+                        optimizer.check_opts_validity()
+
+                        if isinstance(kernel_wrapper_type.kernel_type, Assemble):
+                            optimizer = optimizer.copy_without_vectorization()
+
                         platform_dep_kernels.append(
                             optimizer.apply_to_kernel(
                                 kernel_function, dim, integration_info.loop_strategy
@@ -1242,7 +1600,37 @@ class HyTeGElementwiseOperator:
 
                 # Kernel function call(s).
                 kernel_function_call_strings = []
-                for kernel_function in kernel_functions:
+                for kernel_function, integration_info in zip(
+                    kernel_functions, integration_infos
+                ):
+
+                    pre_call_code = ""
+                    post_call_code = ""
+
+                    if (
+                        integration_info.integration_domain
+                        == MacroIntegrationDomain.DOMAIN_BOUNDARY
+                    ):
+
+                        if not isinstance(integration_info.loop_strategy, BOUNDARY):
+                            raise HOGException(
+                                "The loop strategy should be BOUNDARY for boundary integrals."
+                            )
+
+                        facet_type = "Edge" if dim == 2 else "Face"
+
+                        neighbor_facet = (
+                            f"getStorage()->get{facet_type}( "
+                            f"{macro_type[dim]}.getLowerDimNeighbors()"
+                            f"[{integration_info.loop_strategy.facet_id}] )"
+                        )
+
+                        pre_call_code = (
+                            f"if ( boundaryCondition_.getBoundaryUIDFromMeshFlag( "
+                            f"{neighbor_facet}->getMeshBoundaryFlag() ) == {integration_info.boundary_uid_name}_ ) {{"
+                        )
+                        post_call_code = "}"
+
                     kernel_parameters = kernel_function.get_parameters()
 
                     kernel_function_call_parameter_string = ",\n".join(
@@ -1251,8 +1639,10 @@ class HyTeGElementwiseOperator:
 
                     kernel_function_call_strings.append(
                         f"""
+                                    {pre_call_code}\n
                                     {kernel_function.function_name}(\n
-                                    {kernel_function_call_parameter_string});"""
+                                    {kernel_function_call_parameter_string});\n
+                                    {post_call_code}\n"""
                     )
 
                 kernel_wrapper_type.substitute(
diff --git a/hog/operator_generation/optimizer.py b/hog/operator_generation/optimizer.py
index 6d433c4b027553fd681954c9411f47fdc450c75d..e9922a24ea34fd3a628c5e37dd6f9a81c8345a93 100644
--- a/hog/operator_generation/optimizer.py
+++ b/hog/operator_generation/optimizer.py
@@ -18,7 +18,7 @@ from copy import deepcopy
 import enum
 import logging
 import sympy as sp
-from typing import Dict, Iterable, List, Set
+from typing import Dict, Iterable, List, Set, Any
 
 from pystencils import TypedSymbol
 from pystencils.astnodes import (
@@ -108,6 +108,11 @@ class Optimizer:
     def __getitem__(self, opt):
         return opt in self._opts
 
+    def copy_without_vectorization(
+        self,
+    ) -> Any:  # Optimizer and Self are not accepted :/
+        return Optimizer(self._opts - {Opts.VECTORIZE, Opts.VECTORIZE512})
+
     def check_opts_validity(self) -> None:
         """Checks if the desired optimizations are valid."""
 
diff --git a/hog/operator_generation/pystencils_extensions.py b/hog/operator_generation/pystencils_extensions.py
index 9aa6dbe4a1d71fb89bd9a1d692cf49c39a90a8b2..8ee01d446f1d28cd2e8460d914d62bdb1f8b4512 100644
--- a/hog/operator_generation/pystencils_extensions.py
+++ b/hog/operator_generation/pystencils_extensions.py
@@ -18,7 +18,7 @@ from typing import Dict, List, Tuple, Union
 
 import sympy as sp
 from pystencils import FieldType, Field
-from pystencils.astnodes import Block, LoopOverCoordinate, Node
+from pystencils.astnodes import Block, LoopOverCoordinate, Node, get_next_parent_of_type
 import pystencils as ps
 from pystencils.astnodes import (
     Block,
@@ -88,6 +88,60 @@ def loop_over_simplex(
     return loops[dim - 1]
 
 
+def loop_over_simplex_facet(dim: int, width: int, facet_id: int) -> LoopOverCoordinate:
+    """
+    Loops over one boundary facet of a simplex (e.g., one edge in 2D, one face in 3D).
+
+    The facet is specified by an integer. It is required that
+
+        0 ≤ facet_id ≤ dim
+
+    Let [x_0, x_1, ..., x_(dim-1)] be the coordinate of one element that is looped over.
+
+    For facet_id < dim, we have that
+
+        x_(dim - 1 - facet_id) = 0
+
+    and the remaining boundary is selected with facet_id == dim.
+
+    So in 2D for example, we get
+
+        facet_id = 0: (x_0, 0  )
+        facet_id = 1: (0,   x_1)
+        facet_id = 2: the xy- (or "diagonal") boundary
+    """
+    loop = loop_over_simplex(dim, width)
+    innermost_loops = get_innermost_loop(loop)
+    if len(innermost_loops) != 1:
+        raise HOGException("There should be only one innermost loop.")
+    innermost_loop: LoopOverCoordinate = innermost_loops[0]
+
+    if facet_id not in range(0, dim + 1):
+        raise HOGException(f"Bad facet_id ({facet_id}) for dim {dim}.")
+
+    if facet_id == dim:
+        # For the "diagonal" loop we can just iterate as usual but skip all but the last element of the innermost loop.
+        # I hope.
+        innermost_loop.start = innermost_loop.stop - 1
+        return loop
+
+    # For the other facet_ids we need to find the corresponding loop and set that counter to 0.
+    # I am doing this here by just traversing the loops from the inside out, assuming that no loop cutting etc.
+    # occurred.
+    loop_with_counter_to_be_set_to_zero = innermost_loop
+    for d in range(dim - 1 - facet_id):
+        loop_with_counter_to_be_set_to_zero = get_next_parent_of_type(
+            loop_with_counter_to_be_set_to_zero, LoopOverCoordinate
+        )
+    if loop_with_counter_to_be_set_to_zero is None:
+        raise HOGException("There was no parent loop. This should not happen. I think.")
+
+    loop_with_counter_to_be_set_to_zero.start = 0
+    loop_with_counter_to_be_set_to_zero.stop = 1
+
+    return loop
+
+
 def create_micro_element_loops(
     dim: int, micro_edges_per_macro_edge: int
 ) -> Dict[Union[FaceType, CellType], LoopOverCoordinate]:
diff --git a/hog/quadrature/quadrature.py b/hog/quadrature/quadrature.py
index b51dd0bfce6014ac3e10c0ce05c89694295d5c20..dc0d39b4e7d49fe816ae05f331bac1a8554915fb 100644
--- a/hog/quadrature/quadrature.py
+++ b/hog/quadrature/quadrature.py
@@ -26,7 +26,6 @@ import sympy as sp
 from hog.element_geometry import (
     ElementGeometry,
     TriangleElement,
-    EmbeddedTriangle,
     TetrahedronElement,
     LineElement,
 )
@@ -51,7 +50,6 @@ def select_quadrule(
     """Checks for availability of a specified quadrature rule and chooses a rule with minimal points
     if only a degree is given."""
 
-    # TODO for now, leave out line elements as we have no use for them without DG
     logger = get_logger()
 
     # quadrule given by name, just check if it exists and return it
@@ -96,24 +94,43 @@ def select_quadrule(
                 warnings.simplefilter("ignore")
 
                 all_schemes = []
-                if isinstance(geometry, TriangleElement) or isinstance(
-                    geometry, EmbeddedTriangle
-                ):
+                if isinstance(geometry, LineElement):
+                    # Since line integrals can be approximated with arbitrary order, registering schemes is not
+                    # meaningful. I doubt that we will need quadrature with order > 20, so we simply limit it here.
+                    # If we require higher orders, simply bump this number.
+                    line_quad_degree_limit = 20
+                    # Also, for now let's restrict ourselves to Gauss-Legendre polynomials.
+                    schemes = {
+                        f"gauss_legendre_{d}": quadpy.c1.gauss_legendre(d)
+                        for d in range(1, line_quad_degree_limit + 1)
+                    }
+                elif isinstance(geometry, TriangleElement):
                     schemes = quadpy.t2.schemes
                 elif isinstance(geometry, TetrahedronElement):
                     schemes = quadpy.t3.schemes
                 for key, s in schemes.items():
                     try:
-                        scheme = s()
+                        if isinstance(geometry, LineElement):
+                            # For some reason the quadpy implementation is a little different for line segments.
+                            scheme = s
+                        else:
+                            scheme = s()
+
                     except TypeError as e:
                         pass
                     all_schemes.append(scheme)
 
             def select_degree(x: TnScheme) -> int:
-                if x.degree >= degree:
-                    return x.points.shape[1]
+                if isinstance(geometry, LineElement):
+                    if x.degree >= degree:
+                        return x.degree
+                    else:
+                        return 10**10000  # just a large number
                 else:
-                    return 10**10000  # just a large number
+                    if x.degree >= degree:
+                        return x.points.shape[1]
+                    else:
+                        return 10**10000  # just a large number
 
             return min(all_schemes, key=lambda x: select_degree(x))
 
@@ -121,9 +138,11 @@ def select_quadrule(
     else:
         raise HOGException(f"Unexpected {scheme_info}")
 
-    logger.info(
-        f"Integrating over {geometry} with rule: {scheme.name} (degree: {scheme.degree}, #points: {scheme.points.shape[1]})."
-    )
+    if isinstance(geometry, LineElement):
+        num_points = len(scheme.points)
+    else:
+        num_points = scheme.points.shape[1]
+
     return scheme
 
 
@@ -226,8 +245,6 @@ class Quadrature:
                     f"Cannot apply quadrature rule to matrix of shape {f.shape}: {f}."
                 )
         ref_symbols = symbolizer.ref_coords_as_list(self._geometry.dimensions)
-        if isinstance(self._geometry, EmbeddedTriangle):
-            ref_symbols = symbolizer.ref_coords_as_list(self._geometry.dimensions - 1)
 
         if self._scheme_name == "exact":
             mat_entry = integrate_exact_over_reference(f, self._geometry, symbolizer)
@@ -245,10 +262,12 @@ class Quadrature:
             for i, (point, weight) in enumerate(zip(inline_points, inline_weights)):
                 spat_coord_subs = {}
                 for idx, symbol in enumerate(ref_symbols):
+                    if not isinstance(point, sp.Matrix):
+                        point = sp.Matrix([point])
                     spat_coord_subs[symbol] = point[idx]
                 if not blending.is_affine():
                     for symbol in symbolizer.quadpoint_dependent_free_symbols(
-                        self._geometry.dimensions
+                        self._geometry.space_dimension
                     ):
                         spat_coord_subs[symbol] = sp.Symbol(symbol.name + f"_q_{i}")
                 f_sub = fast_subs(f, spat_coord_subs)
@@ -291,9 +310,6 @@ class Quadrature:
         elif isinstance(geometry, LineElement):
             vertices = np.asarray([[0.0], [1.0]])
             degree = scheme.degree
-        elif isinstance(geometry, EmbeddedTriangle):
-            vertices = np.asarray([[0.0, 0.0], [1.0, 0.0], [0.0, 1.0]])
-            degree = scheme.degree
         else:
             raise HOGException("Invalid geometry for quadrature.")
 
diff --git a/hog/symbolizer.py b/hog/symbolizer.py
index 721e82000c6707a5fe5ff7069c891fad25b427b2..d31c7524cbeb8feaefc7b47cf61b770d1dfc7ce7 100644
--- a/hog/symbolizer.py
+++ b/hog/symbolizer.py
@@ -18,6 +18,7 @@ from typing import List
 import sympy as sp
 import string
 from hog.exception import HOGException
+from hog.element_geometry import ElementGeometry
 
 
 class Symbolizer:
@@ -167,25 +168,27 @@ class Symbolizer:
     def tmp_prefix(self) -> str:
         return self._tmp_prefix
 
-    def jac_ref_to_affine(self, dimensions: int) -> sp.Matrix:
+    def jac_ref_to_affine(self, geometry: ElementGeometry) -> sp.Matrix:
         return sp.Matrix(
             [
                 [
                     sp.Symbol(f"{self._symbol_jac_affine}_{i}_{j}")
-                    for j in range(dimensions)
+                    for j in range(geometry.dimensions)
                 ]
-                for i in range(dimensions)
+                for i in range(geometry.space_dimension)
             ]
         )
 
-    def jac_ref_to_affine_inv(self, dimensions: int) -> sp.Matrix:
+    def jac_ref_to_affine_inv(self, geometry: ElementGeometry) -> sp.Matrix:
+        if geometry.dimensions != geometry.space_dimension:
+            raise HOGException("Cannot invert Jacobian for embedded elements.")
         return sp.Matrix(
             [
                 [
                     sp.Symbol(f"{self._symbol_jac_affine_inv}_{i}_{j}")
-                    for j in range(dimensions)
+                    for j in range(geometry.dimensions)
                 ]
-                for i in range(dimensions)
+                for i in range(geometry.dimensions)
             ]
         )
 
@@ -216,7 +219,7 @@ class Symbolizer:
 
     def abs_det_jac_affine_to_blending(self, q_pt: str = "") -> sp.Symbol:
         return sp.Symbol(f"{self._symbol_abs_det_jac_blending}{q_pt}")
-    
+
     def hessian_blending_map(self, dimensions: int, q_pt: str = "") -> List[sp.Matrix]:
         return [
             sp.Matrix(
@@ -227,7 +230,8 @@ class Symbolizer:
                     ]
                     for i in range(dimensions)
                 ]
-            ) for k in range(dimensions)
+            )
+            for k in range(dimensions)
         ]
 
     def blending_parameter_symbols(self, num_symbols: int) -> List[sp.Symbol]:
diff --git a/hog/sympy_extensions.py b/hog/sympy_extensions.py
index 3f92ee99ba1dcd4802d435817a208d850cd1043e..5d6467975da0349fafb80d8d84f46857f3e12924 100644
--- a/hog/sympy_extensions.py
+++ b/hog/sympy_extensions.py
@@ -22,7 +22,9 @@ T = TypeVar("T")
 
 
 def fast_subs(
-    expression: T, substitutions: Dict[sp.Expr, sp.Expr], skip: Optional[Callable[[sp.Expr], bool]] = None
+    expression: T,
+    substitutions: Dict[sp.Expr, sp.Expr],
+    skip: Optional[Callable[[sp.Expr], bool]] = None,
 ) -> T:
     """Similar to sympy subs function.
 
diff --git a/hog_tests/operator_generation/test_boundary_loop.py b/hog_tests/operator_generation/test_boundary_loop.py
new file mode 100644
index 0000000000000000000000000000000000000000..34a504e5eef0f153c382e00964fc1af26b812d74
--- /dev/null
+++ b/hog_tests/operator_generation/test_boundary_loop.py
@@ -0,0 +1,107 @@
+# HyTeG Operator Generator
+# Copyright (C) 2024  HyTeG Team
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <https://www.gnu.org/licenses/>.
+import logging
+
+from sympy.core.cache import clear_cache
+
+from hog.blending import AnnulusMap, IcosahedralShellMap
+from hog.element_geometry import LineElement, TriangleElement, TetrahedronElement
+from hog.function_space import LagrangianFunctionSpace
+from hog.operator_generation.operators import (
+    HyTeGElementwiseOperator,
+)
+from hog.symbolizer import Symbolizer
+from hog.quadrature import Quadrature, select_quadrule
+from hog.operator_generation.kernel_types import ApplyWrapper
+from hog.operator_generation.types import hyteg_type
+from hog.forms_boundary import mass_boundary
+from hog.logger import TimedLogger
+
+
+def test_boundary_loop():
+
+    # TimedLogger.set_log_level(logging.DEBUG)
+
+    dims = [2, 3]
+
+    clear_cache()
+
+    symbolizer = Symbolizer()
+
+    name = f"P2MassBoundary"
+
+    trial = LagrangianFunctionSpace(2, symbolizer)
+    test = LagrangianFunctionSpace(2, symbolizer)
+
+    type_descriptor = hyteg_type()
+
+    kernel_types = [
+        ApplyWrapper(
+            test,
+            trial,
+            type_descriptor=type_descriptor,
+            dims=dims,
+        )
+    ]
+
+    operator = HyTeGElementwiseOperator(
+        name,
+        symbolizer=symbolizer,
+        kernel_wrapper_types=kernel_types,
+        type_descriptor=type_descriptor,
+    )
+
+    for dim in dims:
+
+        if dim == 2:
+
+            volume_geometry = TriangleElement()
+            boundary_geometry = LineElement(space_dimension=2)
+            blending = AnnulusMap()
+
+        else:
+
+            volume_geometry = TetrahedronElement()
+            boundary_geometry = TriangleElement(space_dimension=3)
+            blending = IcosahedralShellMap()
+
+        quad = Quadrature(select_quadrule(5, boundary_geometry), boundary_geometry)
+
+        form = mass_boundary(
+            trial,
+            test,
+            volume_geometry,
+            boundary_geometry,
+            symbolizer,
+            blending=blending,
+        )
+
+        operator.add_boundary_integral(
+            name=f"boundary_mass",
+            volume_geometry=volume_geometry,
+            quad=quad,
+            blending=blending,
+            form=form,
+        )
+
+    operator.generate_class_code(
+        ".",
+        clang_format_binary="clang-format",
+    )
+
+
+if __name__ == "__main__":
+    test_boundary_loop()
diff --git a/hog_tests/operator_generation/test_opgen_smoke.py b/hog_tests/operator_generation/test_opgen_smoke.py
index b328de9311ecd64a9ad32fb435e2b0d599212314..747fd53d2b2498766db6c9cd54afc3e89bd6d17c 100644
--- a/hog_tests/operator_generation/test_opgen_smoke.py
+++ b/hog_tests/operator_generation/test_opgen_smoke.py
@@ -16,21 +16,18 @@
 
 from sympy.core.cache import clear_cache
 
-from hog.blending import IdentityMap
 from hog.operator_generation.loop_strategies import CUBES
 from hog.operator_generation.optimizer import Opts
-from hog.element_geometry import TriangleElement
+from hog.element_geometry import LineElement, TriangleElement, TetrahedronElement
 from hog.function_space import LagrangianFunctionSpace
-from hog.operator_generation.operators import (
-    HyTeGElementwiseOperator,
-    MacroIntegrationDomain,
-)
+from hog.operator_generation.operators import HyTeGElementwiseOperator
 from hog.symbolizer import Symbolizer
 from hog.quadrature import Quadrature, select_quadrule
-from hog.forms import div_k_grad, mass
-from hog.operator_generation.kernel_types import ApplyWrapper
+from hog.forms import div_k_grad
+from hog.forms_boundary import mass_boundary
+from hog.operator_generation.kernel_types import ApplyWrapper, AssembleWrapper
 from hog.operator_generation.types import hyteg_type
-from hog.blending import AnnulusMap
+from hog.blending import AnnulusMap, IcosahedralShellMap
 
 
 def test_opgen_smoke():
@@ -41,26 +38,24 @@ def test_opgen_smoke():
 
     We are generating a matvec method here for
 
-        ∫ k ∇u · ∇v dx + ∫ uv dx
+        ∫ k ∇u · ∇v dx + ∫ uv ds
 
     with either integral being evaluated in their own kernel.
 
     That may not be reasonable but tests some features.
     """
+
     clear_cache()
 
     symbolizer = Symbolizer()
-    volume_geometry = TriangleElement()
 
-    name = f"P2DivKGradBlendingPlusMass"
+    name = f"P2DivKGradBlendingPlusBoundaryMass"
+
+    dims = [2, 3]
 
     trial = LagrangianFunctionSpace(2, symbolizer)
     test = LagrangianFunctionSpace(2, symbolizer)
     coeff = LagrangianFunctionSpace(2, symbolizer)
-    quad = Quadrature(select_quadrule(2, volume_geometry), volume_geometry)
-
-    divkgrad = div_k_grad(trial, test, volume_geometry, symbolizer, AnnulusMap(), coeff)
-    m = mass(trial, test, volume_geometry, symbolizer, AnnulusMap())
 
     type_descriptor = hyteg_type()
 
@@ -69,41 +64,72 @@ def test_opgen_smoke():
             test,
             trial,
             type_descriptor=type_descriptor,
-            dims=[2],
-        )
+            dims=dims,
+        ),
+        AssembleWrapper(
+            test,
+            trial,
+            type_descriptor=type_descriptor,
+            dims=dims,
+        ),
     ]
 
-    opts = {Opts.MOVECONSTANTS, Opts.VECTORIZE, Opts.TABULATE, Opts.QUADLOOPS}
-
     operator = HyTeGElementwiseOperator(
         name,
         symbolizer=symbolizer,
         kernel_wrapper_types=kernel_types,
-        opts=opts,
         type_descriptor=type_descriptor,
     )
 
-    operator.add_integral(
-        name="div_k_grad",
-        dim=volume_geometry.dimensions,
-        geometry=volume_geometry,
-        integration_domain=MacroIntegrationDomain.VOLUME,
-        quad=quad,
-        blending=AnnulusMap(),
-        form=divkgrad,
-        loop_strategy=CUBES(),
-    )
+    opts_volume = {Opts.MOVECONSTANTS, Opts.VECTORIZE, Opts.TABULATE, Opts.QUADLOOPS}
+    opts_boundary = {Opts.MOVECONSTANTS}
 
-    operator.add_integral(
-        name="mass",
-        dim=volume_geometry.dimensions,
-        geometry=volume_geometry,
-        integration_domain=MacroIntegrationDomain.VOLUME,
-        quad=quad,
-        blending=AnnulusMap(),
-        form=m,
-        loop_strategy=CUBES(),
-    )
+    for d in dims:
+
+        if d == 2:
+            volume_geometry = TriangleElement()
+            boundary_geometry = LineElement(space_dimension=2)
+            blending_map = AnnulusMap()
+        else:
+            volume_geometry = TetrahedronElement()
+            boundary_geometry = TriangleElement(space_dimension=3)
+            blending_map = IcosahedralShellMap()
+
+        quad_volume = Quadrature(select_quadrule(2, volume_geometry), volume_geometry)
+        quad_boundary = Quadrature(
+            select_quadrule(5, boundary_geometry), boundary_geometry
+        )
+
+        divkgrad = div_k_grad(
+            trial, test, volume_geometry, symbolizer, blending_map, coeff
+        )
+        mass_b = mass_boundary(
+            trial,
+            test,
+            volume_geometry,
+            boundary_geometry,
+            symbolizer,
+            blending_map,
+        )
+
+        operator.add_volume_integral(
+            name="div_k_grad",
+            volume_geometry=volume_geometry,
+            quad=quad_volume,
+            blending=blending_map,
+            form=divkgrad,
+            loop_strategy=CUBES(),
+            optimizations=opts_volume,
+        )
+
+        operator.add_boundary_integral(
+            name="mass_boundary",
+            volume_geometry=volume_geometry,
+            quad=quad_boundary,
+            blending=blending_map,
+            form=mass_b,
+            optimizations=opts_boundary,
+        )
 
     operator.generate_class_code(
         ".",