diff --git a/generate_all_hyteg_forms.py b/generate_all_hyteg_forms.py
index 313c5a185c90b50c7f510317793eb1b4e4c0eba8..e59ef72ae8e198132ab6fd19282af32bd3a1ea7b 100644
--- a/generate_all_hyteg_forms.py
+++ b/generate_all_hyteg_forms.py
@@ -23,9 +23,15 @@ import itertools
 import sympy as sp
 from sympy.core.cache import clear_cache
 import re
+import quadpy
 
 from hog.blending import GeometryMap, IdentityMap, ExternalMap, AnnulusMap
-from hog.element_geometry import TriangleElement, TetrahedronElement, ElementGeometry
+from hog.element_geometry import (
+    TriangleElement,
+    TetrahedronElement,
+    EmbeddedTriangle,
+    ElementGeometry,
+)
 from hog.function_space import FunctionSpace, LagrangianFunctionSpace, N1E1Space
 from hog.forms import (
     mass,
@@ -40,6 +46,16 @@ from hog.forms import (
     divdiv,
     supg_diffusion,
 )
+from hog.manifold_forms import (
+    laplace_beltrami,
+    manifold_mass,
+    manifold_vector_mass,
+    manifold_normal_penalty,
+    manifold_divergence,
+    manifold_vector_divergence,
+    manifold_epsilon,
+    vector_laplace_beltrami,
+)
 from hog.forms_vectorial import mass_n1e1, curl_curl
 from hog.quadrature import Quadrature, select_quadrule
 from hog.exception import HOGException
@@ -433,7 +449,28 @@ form_infos = [
         test_degree=2,
         quad_schemes={2: 4, 3: 4},
         blending=AnnulusMap(),
-    )
+    ),
+    FormInfo(
+        "laplace_beltrami",
+        trial_degree=1,
+        test_degree=1,
+        quad_schemes={3: 1},
+    ),
+    FormInfo(
+        "laplace_beltrami",
+        trial_degree=1,
+        test_degree=1,
+        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={3: 3},
+        blending=ExternalMap(),
+    ),
 ]
 
 for d in [1, 2]:
@@ -566,6 +603,135 @@ for trial_deg, test_deg, transpose in [
                 )
             )
 
+for blending in [IdentityMap(), ExternalMap()]:
+    form_infos.append(
+        FormInfo(
+            "manifold_vector_mass",
+            trial_degree=2,
+            test_degree=2,
+            quad_schemes={3: 3},
+            row_dim=3,
+            col_dim=3,
+            is_implemented=is_implemented_for_vector_to_vector,
+            blending=blending,
+        )
+    )
+
+    form_infos.append(
+        FormInfo(
+            "manifold_normal_penalty",
+            trial_degree=2,
+            test_degree=2,
+            quad_schemes={3: 3},
+            row_dim=3,
+            col_dim=3,
+            is_implemented=is_implemented_for_vector_to_vector,
+            blending=blending,
+        )
+    )
+
+    form_infos.append(
+        FormInfo(
+            "manifold_epsilon",
+            trial_degree=2,
+            test_degree=2,
+            quad_schemes={3: 3},
+            row_dim=3,
+            col_dim=3,
+            is_implemented=is_implemented_for_vector_to_vector,
+            blending=blending,
+        )
+    )
+
+    form_infos.append(
+        FormInfo(
+            "manifold_epsilon",
+            trial_degree=2,
+            test_degree=2,
+            quad_schemes={3: 6},
+            row_dim=3,
+            col_dim=3,
+            is_implemented=is_implemented_for_vector_to_vector,
+            blending=blending,
+        )
+    )
+
+    form_infos.append(
+        FormInfo(
+            "vector_laplace_beltrami",
+            trial_degree=2,
+            test_degree=2,
+            quad_schemes={3: 3},
+            row_dim=3,
+            col_dim=3,
+            is_implemented=is_implemented_for_vector_to_vector,
+            blending=blending,
+        )
+    )
+
+for trial_deg, test_deg, transpose in [(1, 2, True), (2, 1, False)]:
+    for blending in [IdentityMap(), ExternalMap()]:
+        if not transpose:
+            form_infos.append(
+                FormInfo(
+                    "manifold_div",
+                    trial_degree=trial_deg,
+                    test_degree=test_deg,
+                    quad_schemes={3: 3},
+                    row_dim=1,
+                    col_dim=3,
+                    is_implemented=is_implemented_for_vector_to_scalar,
+                    blending=blending,
+                )
+            )
+        else:
+            form_infos.append(
+                FormInfo(
+                    "manifold_divt",
+                    trial_degree=trial_deg,
+                    test_degree=test_deg,
+                    quad_schemes={3: 3},
+                    row_dim=3,
+                    col_dim=1,
+                    is_implemented=is_implemented_for_scalar_to_vector,
+                    blending=blending,
+                )
+            )
+
+for trial_deg, test_deg, transpose in [
+    (1, 2, True),
+    (2, 1, False),
+    (0, 2, True),
+    (2, 0, False),
+]:
+    for blending in [IdentityMap(), ExternalMap()]:
+        if not transpose:
+            form_infos.append(
+                FormInfo(
+                    "manifold_vector_div",
+                    trial_degree=trial_deg,
+                    test_degree=test_deg,
+                    quad_schemes={3: 3},
+                    row_dim=1,
+                    col_dim=3,
+                    is_implemented=is_implemented_for_vector_to_scalar,
+                    blending=blending,
+                )
+            )
+        else:
+            form_infos.append(
+                FormInfo(
+                    "manifold_vector_divt",
+                    trial_degree=trial_deg,
+                    test_degree=test_deg,
+                    quad_schemes={3: 3},
+                    row_dim=3,
+                    col_dim=1,
+                    is_implemented=is_implemented_for_scalar_to_vector,
+                    blending=blending,
+                )
+            )
+
 
 def form_func(
     name: str,
@@ -675,6 +841,94 @@ def form_func(
         ).integrate(quad, symbolizer)
     elif name.startswith("supg_d"):
         raise HOGException(f"SUPG Diffusion is not supported for form generation")
+    elif name.startswith("laplace_beltrami"):
+        return laplace_beltrami(
+            trial, test, geometry, symbolizer, blending=blending
+        ).integrate(quad, symbolizer)
+    elif name.startswith("manifold_mass"):
+        return manifold_mass(
+            trial, test, geometry, symbolizer, blending=blending
+        ).integrate(quad, symbolizer)
+    elif name.startswith("manifold_vector_mass"):
+        return manifold_vector_mass(
+            trial,
+            test,
+            geometry,
+            symbolizer,
+            blending=blending,
+            component_trial=col,
+            component_test=row,
+        ).integrate(quad, symbolizer)
+    elif name.startswith("manifold_normal_penalty"):
+        return manifold_normal_penalty(
+            trial,
+            test,
+            geometry,
+            symbolizer,
+            blending=blending,
+            component_trial=col,
+            component_test=row,
+        ).integrate(quad, symbolizer)
+    elif name.startswith("manifold_divt"):
+        return manifold_divergence(
+            trial,
+            test,
+            geometry,
+            symbolizer,
+            blending=blending,
+            component_index=row,
+            transpose=True,
+        ).integrate(quad, symbolizer)
+    elif name.startswith("manifold_div"):
+        return manifold_divergence(
+            trial,
+            test,
+            geometry,
+            symbolizer,
+            blending=blending,
+            component_index=col,
+            transpose=False,
+        ).integrate(quad, symbolizer)
+    elif name.startswith("manifold_vector_divt"):
+        return manifold_vector_divergence(
+            trial,
+            test,
+            geometry,
+            symbolizer,
+            blending=blending,
+            component_index=row,
+            transpose=True,
+        ).integrate(quad, symbolizer)
+    elif name.startswith("manifold_vector_div"):
+        return manifold_vector_divergence(
+            trial,
+            test,
+            geometry,
+            symbolizer,
+            blending=blending,
+            component_index=col,
+            transpose=False,
+        ).integrate(quad, symbolizer)
+    elif name.startswith("manifold_epsilon"):
+        return manifold_epsilon(
+            trial,
+            test,
+            geometry,
+            symbolizer,
+            blending=blending,
+            component_trial=col,
+            component_test=row,
+        ).integrate(quad, symbolizer)
+    elif name.startswith("vector_laplace_beltrami"):
+        return vector_laplace_beltrami(
+            trial,
+            test,
+            geometry,
+            symbolizer,
+            blending=blending,
+            component_trial=col,
+            component_test=row,
+        ).integrate(quad, symbolizer)
     else:
         raise HOGException(f"Cannot call form function with name {name}.")
 
@@ -702,7 +956,7 @@ def parse_arguments():
         type=str,
         help="build form(s) only for triangle (2D) or tetrahedron (3D) elements; if not speficied we do both",
         nargs="?",
-        choices=["triangle", "tetrahedron", "both"],
+        choices=["triangle", "tetrahedron", "embedded_triangle", "both"],
         default="both",
     )
     parser.add_argument(
@@ -800,6 +1054,9 @@ def main():
     elif args.geometry == "tetrahedron":
         logger.info(f"- selected geometry: tetrahedron")
         geometries = [TetrahedronElement()]
+    elif args.geometry == "embedded_triangle":
+        logger.info(f"- selected geometry: embedded triangle")
+        geometries = [EmbeddedTriangle()]
     else:
         logger.info(f"- selected geometries: triangle, tetrahedron")
         geometries = [TriangleElement(), TetrahedronElement()]
@@ -838,11 +1095,14 @@ def main():
                             f"- Generating code for class {form_info.class_name(row,col)}, {geometry.dimensions}D"
                         ):
                             quad = Quadrature(
-                                select_quadrule(form_info.quad_schemes[geometry.dimensions], geometry),
-                                # form_info.quad_schemes[geometry.dimensions],
+                                select_quadrule(
+                                    form_info.quad_schemes[geometry.dimensions],
+                                    geometry,
+                                ),
                                 geometry,
                                 inline_values=form_info.inline_quad,
                             )
+
                             mat = form_func(
                                 form_info.form_name,
                                 row,
diff --git a/hog/element_geometry.py b/hog/element_geometry.py
index 0dc03539006370c33d12d1b12ebf6a8e84afb4c4..b69c3e95945c5cdf59ee4e05e46f7d807b9067cd 100644
--- a/hog/element_geometry.py
+++ b/hog/element_geometry.py
@@ -14,6 +14,7 @@
 # You should have received a copy of the GNU General Public License
 # along with this program.  If not, see <https://www.gnu.org/licenses/>.
 
+
 class ElementGeometry:
     def __init__(self, dimensions: int, num_vertices: int):
         self.dimensions = dimensions
@@ -59,6 +60,17 @@ class TriangleElement(ElementGeometry):
         return str(self)
 
 
+class EmbeddedTriangle(ElementGeometry):
+    def __init__(self):
+        super().__init__(3, 3)
+
+    def __str__(self):
+        return f"embedded triangle, dim: 3, vertices: 3"
+
+    def __repr__(self):
+        return str(self)
+
+
 class TetrahedronElement(ElementGeometry):
     def __init__(self):
         super().__init__(3, 4)
@@ -67,4 +79,4 @@ class TetrahedronElement(ElementGeometry):
         return f"tetrahedron, dim: 3, vertices: 4"
 
     def __repr__(self):
-        return str(self)
\ No newline at end of file
+        return str(self)
diff --git a/hog/external_functions.py b/hog/external_functions.py
index c22943568e72627f142cdb35cc116b95c8c5c8c6..0180c9cc6ef912980de4ded5eebcc15989212aab 100644
--- a/hog/external_functions.py
+++ b/hog/external_functions.py
@@ -35,6 +35,25 @@ geometryMap_->evalF( in, out );
 *out_1 = out[1];"""
 
 
+class BlendingFEmbeddedTriangle(MultiAssignment):
+    def function_name(self):
+        return "Blending_F_EmbeddedTriangle"
+
+    def num_input_args(self):
+        return 3
+
+    def num_output_args(self):
+        return 3
+
+    def implementation(self):
+        return """Point3D  in( {in_0, in_1, in_2} );
+Point3D out;
+geometryMap_->evalF( in, out );
+*out_0 = out[0];
+*out_1 = out[1];
+*out_2 = out[2];"""
+
+
 class BlendingFTetrahedron(MultiAssignment):
     def function_name(self):
         return "Blending_F_Tetrahedron"
@@ -76,6 +95,33 @@ geometryMap_->evalDF( mappedPt, DPsi );
 *out_3 = DPsi( 1, 1 );"""
 
 
+class BlendingDFEmbeddedTriangle(MultiAssignment):
+    def function_name(self):
+        return "Blending_DF_EmbeddedTriangle"
+
+    @classmethod
+    def num_input_args(cls):
+        return 3
+
+    @classmethod
+    def num_output_args(cls):
+        return 3 * 3
+
+    def implementation(self):
+        return """Point3D  mappedPt( {in_0, in_1, in_2} );
+Matrix3r DPsi;
+geometryMap_->evalDF( mappedPt, DPsi );
+*out_0 = DPsi( 0, 0 );
+*out_1 = DPsi( 0, 1 );
+*out_2 = DPsi( 0, 2 );
+*out_3 = DPsi( 1, 0 );
+*out_4 = DPsi( 1, 1 );
+*out_5 = DPsi( 1, 2 );
+*out_6 = DPsi( 2, 0 );
+*out_7 = DPsi( 2, 1 );
+*out_8 = DPsi( 2, 2 );"""
+
+
 class BlendingDFInvDFTriangle(MultiAssignment):
     def function_name(self):
         return "Blending_DFInvDF_Triangle"
diff --git a/hog/fem_helpers.py b/hog/fem_helpers.py
index 13c5dff89aad186e7034e229812d13a5678e1f5b..a6a525fbb850ea8cdc317ec794c428d6c40c8bbe 100644
--- a/hog/fem_helpers.py
+++ b/hog/fem_helpers.py
@@ -28,6 +28,7 @@ from hog.blending import (
 from hog.element_geometry import (
     ElementGeometry,
     TriangleElement,
+    EmbeddedTriangle,
     TetrahedronElement,
     LineElement,
 )
@@ -38,10 +39,12 @@ from hog.multi_assignment import MultiAssignment
 from hog.symbolizer import Symbolizer
 from hog.external_functions import (
     BlendingFTriangle,
+    BlendingFEmbeddedTriangle,
     BlendingFTetrahedron,
     BlendingDFTetrahedron,
     BlendingDFTriangle,
     BlendingDFInvDFTriangle,
+    BlendingDFEmbeddedTriangle,
     ScalarVariableCoefficient2D,
     ScalarVariableCoefficient3D,
     VectorVariableCoefficient3D,
@@ -115,6 +118,9 @@ 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
@@ -193,6 +199,9 @@ 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)
 
@@ -211,6 +220,8 @@ def trafo_ref_to_physical(
         blending_class: type[MultiAssignment]
         if isinstance(geometry, TriangleElement):
             blending_class = BlendingFTriangle
+        elif isinstance(geometry, EmbeddedTriangle):
+            blending_class = BlendingDFEmbeddedTriangle
         elif isinstance(geometry, TetrahedronElement):
             blending_class = BlendingFTetrahedron
         else:
@@ -235,6 +246,8 @@ def jac_affine_to_physical(
     blending_class: type[MultiAssignment]
     if isinstance(geometry, TriangleElement):
         blending_class = BlendingDFTriangle
+    elif isinstance(geometry, EmbeddedTriangle):
+        blending_class = BlendingDFEmbeddedTriangle
     elif isinstance(geometry, TetrahedronElement):
         blending_class = BlendingDFTetrahedron
     else:
@@ -267,6 +280,7 @@ def jac_blending_evaluate(
     jac = blending.jacobian(trafo_ref_to_affine(geometry, symbolizer, affine_points))
     return jac
 
+
 def hess_blending_evaluate(
     symbolizer: Symbolizer, geometry: ElementGeometry, blending: GeometryMap
 ) -> List[sp.Matrix]:
@@ -276,6 +290,7 @@ def hess_blending_evaluate(
     hess = blending.hessian(trafo_ref_to_affine(geometry, symbolizer, affine_points))
     return hess
 
+
 def abs_det_jac_blending_eval_symbols(
     geometry: ElementGeometry, symbolizer: Symbolizer, q_pt: str = ""
 ) -> sp.Expr:
diff --git a/hog/function_space.py b/hog/function_space.py
index 7d0823a103fb946f7fda56c8dd33657a405e3cb3..8ee718ed5551fb85631fc5a49066fe16fa974889 100644
--- a/hog/function_space.py
+++ b/hog/function_space.py
@@ -18,7 +18,12 @@ from pyclbr import Function
 from typing import Any, List, Optional, Protocol
 import sympy as sp
 
-from hog.element_geometry import ElementGeometry, TriangleElement, TetrahedronElement
+from hog.element_geometry import (
+    ElementGeometry,
+    TriangleElement,
+    EmbeddedTriangle,
+    TetrahedronElement,
+)
 from hog.exception import HOGException
 from hog.math_helpers import grad, hessian
 from hog.symbolizer import Symbolizer
@@ -84,6 +89,8 @@ 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)
@@ -93,7 +100,7 @@ class FunctionSpace(Protocol):
         raise HOGException(
             f"Gradient of shape function not available for domain type {domain}"
         )
-    
+
     def hessian_shape(
         self,
         geometry: ElementGeometry,
@@ -208,6 +215,40 @@ class LagrangianFunctionSpace(FunctionSpace):
                     -4 * x**2 - 4 * x * y + 4 * x,
                 ]
 
+            elif (
+                isinstance(geometry, EmbeddedTriangle)
+                and self.family in ["Lagrange"]
+                and self._degree == 0
+            ):
+                basis_functions = [sp.sympify(1)]
+
+            elif (
+                isinstance(geometry, EmbeddedTriangle)
+                and self.family in ["Lagrange"]
+                and self._degree == 1
+            ):
+                basis_functions = [
+                    1 - symbols[0] - symbols[1],
+                    symbols[0],
+                    symbols[1],
+                ]
+
+            elif (
+                isinstance(geometry, EmbeddedTriangle)
+                and self.family in ["Lagrange"]
+                and self._degree == 2
+            ):
+                x = symbols[0]
+                y = symbols[1]
+                basis_functions = [
+                    2 * x**2 + 4 * x * y - 3 * x + 2 * y**2 - 3 * y + 1,
+                    2 * x**2 - x,
+                    2 * y**2 - y,
+                    4 * x * y,
+                    -4 * x * y - 4 * y**2 + 4 * y,
+                    -4 * x**2 - 4 * x * y + 4 * x,
+                ]
+
             elif (
                 isinstance(geometry, TetrahedronElement)
                 and self.family in ["Lagrange"]
diff --git a/hog/manifold_forms.py b/hog/manifold_forms.py
new file mode 100644
index 0000000000000000000000000000000000000000..f53fcfed70272186bfd12aa2cc7afb418bc7bb9c
--- /dev/null
+++ b/hog/manifold_forms.py
@@ -0,0 +1,661 @@
+# 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
+import sympy as sp
+
+from hog.element_geometry import ElementGeometry
+from hog.element_geometry import EmbeddedTriangle
+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,
+    create_empty_element_matrix,
+    element_matrix_iterator,
+)
+from hog.function_space import FunctionSpace
+from hog.math_helpers import dot, inv, abs, det, double_contraction, e_vec
+from hog.quadrature import Tabulation
+from hog.symbolizer import Symbolizer
+from hog.logger import TimedLogger
+from hog.blending import GeometryMap, ExternalMap, IdentityMap
+from hog.forms import Form
+
+from hog.manifold_helpers import face_projection, embedded_normal
+
+
+def laplace_beltrami(
+    trial: FunctionSpace,
+    test: FunctionSpace,
+    geometry: ElementGeometry,
+    symbolizer: Symbolizer,
+    blending: GeometryMap = IdentityMap(),
+) -> Form:
+    docstring = f"""
+Laplace beltrami operator.
+
+Geometry map: {blending}
+
+Weak formulation
+
+    u: trial function (space: {trial})
+    v: test function  (space: {test})
+    G: first fundamental form of manifold
+
+    ∫ ∇u · G^(-1) · ∇v · (det(G))^0.5
+"""
+
+    if trial != test:
+        raise HOGException(
+            "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")
+
+    with TimedLogger("assembling laplace beltrami matrix", level=logging.DEBUG):
+        tabulation = Tabulation(symbolizer)
+
+        jac_affine = jac_ref_to_affine(geometry, symbolizer)
+
+        if isinstance(blending, ExternalMap):
+            jac_blending = jac_affine_to_physical(geometry, symbolizer)
+        else:
+            jac_blending = blending.jacobian(trafo_ref_to_affine(geometry, symbolizer))
+
+        fundamental_form = jac_affine.T * jac_blending.T * jac_blending * jac_affine
+        with TimedLogger("inverting first fundamental form", level=logging.DEBUG):
+            fundamental_form_inv = inv(fundamental_form)
+        fundamental_form_det = abs(det(fundamental_form))
+
+        mat = create_empty_element_matrix(trial, test, geometry)
+        it = element_matrix_iterator(trial, test, geometry)
+
+        with TimedLogger(
+            f"integrating {mat.shape[0] * mat.shape[1]} expressions",
+            level=logging.DEBUG,
+        ):
+            for data in it:
+                with TimedLogger(
+                    f"Integrating row = {data.row} , col = {data.col}",
+                    level=logging.DEBUG,
+                ):
+                    grad_phi = data.trial_shape_grad
+                    grad_psi = data.test_shape_grad
+                    laplace_beltrami_symbol = sp.Matrix(
+                        tabulation.register_factor(
+                            "laplace_beltrami_symbol",
+                            dot(
+                                grad_phi,
+                                fundamental_form_inv * grad_psi,
+                            )
+                            * (fundamental_form_det**0.5),
+                        )
+                    )
+                    form = laplace_beltrami_symbol[0]
+                    mat[data.row, data.col] = form
+
+    return Form(mat, tabulation, symmetric=True, docstring=docstring)
+
+
+def manifold_mass(
+    trial: FunctionSpace,
+    test: FunctionSpace,
+    geometry: ElementGeometry,
+    symbolizer: Symbolizer,
+    blending: GeometryMap = IdentityMap(),
+) -> Form:
+    docstring = f"""
+Manifold mass operator.
+
+Geometry map: {blending}
+
+Weak formulation
+
+    u: trial function (space: {trial})
+    v: test function  (space: {test})
+    G: first fundamental form of manifold
+
+    ∫ uv · (det(G))^0.5
+"""
+
+    if trial != test:
+        raise HOGException(
+            "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.")
+
+    with TimedLogger("assembling laplace beltrami matrix", level=logging.DEBUG):
+        tabulation = Tabulation(symbolizer)
+
+        jac_affine = jac_ref_to_affine(geometry, symbolizer)
+
+        if isinstance(blending, ExternalMap):
+            jac_blending = jac_affine_to_physical(geometry, symbolizer)
+        else:
+            jac_blending = blending.jacobian(trafo_ref_to_affine(geometry, symbolizer))
+
+        fundamental_form_det = abs(
+            det(jac_affine.T * jac_blending.T * jac_blending * jac_affine)
+        )
+
+        mat = create_empty_element_matrix(trial, test, geometry)
+        it = element_matrix_iterator(trial, test, geometry)
+
+        with TimedLogger(
+            f"integrating {mat.shape[0] * mat.shape[1]} expressions",
+            level=logging.DEBUG,
+        ):
+            for data in it:
+                with TimedLogger(
+                    f"Integrating row = {data.row} , col = {data.col}",
+                    level=logging.DEBUG,
+                ):
+                    phi = data.trial_shape
+                    psi = data.test_shape
+                    manifold_mass_symbol = sp.Matrix(
+                        tabulation.register_factor(
+                            "manifold_mass_symbol",
+                            sp.Matrix([phi * psi * fundamental_form_det**0.5]),
+                        )
+                    )
+                    form = manifold_mass_symbol[0]
+                    mat[data.row, data.col] = form
+
+    return Form(mat, tabulation, symmetric=True, docstring=docstring)
+
+
+def manifold_vector_mass(
+    trial: FunctionSpace,
+    test: FunctionSpace,
+    geometry: ElementGeometry,
+    symbolizer: Symbolizer,
+    blending: GeometryMap = IdentityMap(),
+    component_trial: int = 0,
+    component_test: int = 0,
+) -> Form:
+    docstring = f"""
+Manifold vector mass operator operator.
+
+Component trial: {component_trial}
+Component test:  {component_test}
+Geometry map:    {blending}
+
+Weak formulation
+
+    u: trial function (vectorial space: {trial})
+    v: test function  (vectorial space: {test})
+    P: projection matrix onto face
+
+    ∫ Pu · Pv · (det(G))^0.5
+    
+"""
+    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.")
+
+        projection = face_projection(geometry, symbolizer, blending=blending)
+
+        jac_affine = jac_ref_to_affine(geometry, symbolizer)
+
+        if isinstance(blending, ExternalMap):
+            jac_blending = jac_affine_to_physical(geometry, symbolizer)
+        else:
+            jac_blending = sp.eye(3)
+
+        fundamental_form_det = abs(
+            det(jac_affine.T * jac_blending.T * jac_blending * jac_affine)
+        )
+
+        mat = create_empty_element_matrix(trial, test, geometry)
+        it = element_matrix_iterator(trial, test, geometry)
+
+        for data in it:
+            phi = data.trial_shape
+            psi = data.test_shape
+            phi_vec = e_vec(geometry.dimensions, component_trial) * phi
+            psi_vec = e_vec(geometry.dimensions, component_test) * psi
+            projected_phi = projection * phi_vec
+            projected_psi = projection * psi_vec
+
+            form = sp.Matrix(
+                tabulation.register_factor(
+                    "manifold_vector_mass_symbol",
+                    dot(projected_phi, projected_psi) * fundamental_form_det**0.5,
+                )
+            )[0]
+            mat[data.row, data.col] = form
+
+    return Form(
+        mat,
+        tabulation,
+        symmetric=component_trial == component_test,
+        docstring=docstring,
+    )
+
+
+def manifold_normal_penalty(
+    trial: FunctionSpace,
+    test: FunctionSpace,
+    geometry: ElementGeometry,
+    symbolizer: Symbolizer,
+    blending: GeometryMap = IdentityMap(),
+    component_trial: int = 0,
+    component_test: int = 0,
+) -> Form:
+    docstring = f"""
+Manifold normal penaly operator.
+
+Component trial: {component_trial}
+Component test:  {component_test}
+Geometry map:    {blending}
+
+Weak formulation
+
+    u: trial function (vectorial space: {trial})
+    v: test function  (vectorial space: {test})
+    n: normal vector of the face
+
+    ∫ (n · u)(n · v) · (det(G))^0.5
+    
+"""
+    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.")
+
+        normal = embedded_normal(geometry, symbolizer, blending)
+
+        jac_affine = jac_ref_to_affine(geometry, symbolizer)
+
+        if isinstance(blending, ExternalMap):
+            jac_blending = jac_affine_to_physical(geometry, symbolizer)
+        else:
+            jac_blending = sp.eye(3)
+
+        fundamental_form_det = abs(
+            det(jac_affine.T * jac_blending.T * jac_blending * jac_affine)
+        )
+
+        mat = create_empty_element_matrix(trial, test, geometry)
+        it = element_matrix_iterator(trial, test, geometry)
+
+        for data in it:
+            phi = data.trial_shape
+            psi = data.test_shape
+            phi_vec = e_vec(geometry.dimensions, component_trial) * phi
+            psi_vec = e_vec(geometry.dimensions, component_test) * psi
+            phi_normal = dot(phi_vec, normal)
+            psi_normal = dot(psi_vec, normal)
+
+            form = sp.Matrix(
+                tabulation.register_factor(
+                    "manifold_normal_penalty_symbol",
+                    phi_normal * psi_normal * fundamental_form_det**0.5,
+                )
+            )[0]
+            mat[data.row, data.col] = form
+
+    return Form(
+        mat,
+        tabulation,
+        symmetric=component_trial == component_test,
+        docstring=docstring,
+    )
+
+
+def manifold_divergence(
+    trial: FunctionSpace,
+    test: FunctionSpace,
+    geometry: ElementGeometry,
+    symbolizer: Symbolizer,
+    blending: GeometryMap = IdentityMap(),
+    component_index: int = 0,
+    transpose: bool = False,
+) -> Form:
+    docstring = f"""
+Manifold divergence operator.
+
+Component index: {component_index}
+Geometry map:    {blending}
+
+Weak formulation
+
+    u: trial function (vectorial space: {trial})
+    v: test function  (space: {test})
+
+    ∫ u · grad_Gamma(v) · (det(G))^0.5
+    
+"""
+
+    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.")
+
+        jac_affine = jac_ref_to_affine(geometry, symbolizer)
+
+        if isinstance(blending, ExternalMap):
+            jac_blending = jac_affine_to_physical(geometry, symbolizer)
+        else:
+            jac_blending = sp.eye(3)
+
+        jac_total = jac_blending * jac_affine
+        fundamental_form = jac_total.T * jac_total
+        fundamental_form_inv = inv(fundamental_form)
+        fundamental_form_det = abs(det(fundamental_form))
+
+        mat = create_empty_element_matrix(trial, test, geometry)
+        it = element_matrix_iterator(trial, test, geometry)
+
+        for data in it:
+            if not transpose:
+                phi = data.trial_shape
+                phi_grad = data.test_shape_grad
+            else:
+                phi = data.test_shape
+                phi_grad = data.trial_shape_grad
+
+            form = (
+                (jac_total * fundamental_form_inv * phi_grad)[component_index]
+                * phi
+                * fundamental_form_det**0.5
+            )
+
+            mat[data.row, data.col] = form
+
+    return Form(
+        mat,
+        tabulation,
+        symmetric=False,
+        docstring=docstring,
+    )
+
+
+def manifold_vector_divergence(
+    trial: FunctionSpace,
+    test: FunctionSpace,
+    geometry: ElementGeometry,
+    symbolizer: Symbolizer,
+    blending: GeometryMap = IdentityMap(),
+    component_index: int = 0,
+    transpose: bool = False,
+) -> Form:
+    docstring = f"""
+Manifold vector divergence operator.
+
+Component index: {component_index}
+Geometry map:    {blending}
+
+Weak formulation
+
+    u: trial function (vectorial space: {trial})
+    v: test function  (space: {test})
+    P: projection matrix
+    
+    ∫ div_Gamma(Pu) · v · (det(G))^0.5
+    
+"""
+
+    with TimedLogger("assembling manifold div matrix", level=logging.DEBUG):
+        tabulation = Tabulation(symbolizer)
+
+        logging.info(
+            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.")
+
+        projection_mat = face_projection(geometry, symbolizer, blending=blending)
+
+        jac_affine = jac_ref_to_affine(geometry, symbolizer)
+
+        if isinstance(blending, ExternalMap):
+            jac_blending = jac_affine_to_physical(geometry, symbolizer)
+        else:
+            jac_blending = sp.eye(3)
+
+        jac_total = jac_blending * jac_affine
+        fundamental_form = jac_total.T * jac_total
+        fundamental_form_inv = inv(fundamental_form)
+        fundamental_form_det = abs(det(fundamental_form))
+
+        mat = create_empty_element_matrix(trial, test, geometry)
+        it = element_matrix_iterator(trial, test, geometry)
+
+        for data in it:
+            ref_symbols_list = symbolizer.ref_coords_as_list(geometry.dimensions - 1)
+            if not transpose:
+                phi_vec = (
+                    projection_mat
+                    * (
+                        e_vec(geometry.dimensions, component_index) * data.trial_shape
+                    ).jacobian(ref_symbols_list)
+                ).T
+                phi = data.test_shape
+            else:
+                phi = data.trial_shape
+                phi_vec = (
+                    projection_mat
+                    * (
+                        e_vec(geometry.dimensions, component_index) * data.test_shape
+                    ).jacobian(ref_symbols_list)
+                ).T
+
+            form = (
+                (jac_total * fundamental_form_inv * phi_vec).trace()
+                * phi
+                * fundamental_form_det**0.5
+            )
+
+            mat[data.row, data.col] = form
+
+    return Form(
+        mat,
+        tabulation,
+        symmetric=False,
+        docstring=docstring,
+    )
+
+
+def manifold_epsilon(
+    trial: FunctionSpace,
+    test: FunctionSpace,
+    geometry: ElementGeometry,
+    symbolizer: Symbolizer,
+    blending: GeometryMap = IdentityMap(),
+    component_trial: int = 0,
+    component_test: int = 0,
+) -> Form:
+    docstring = f"""
+Manifold epsilon operator.
+
+Component trial: {component_trial}
+Component test:  {component_test}
+Geometry map:    {blending}
+
+Weak formulation
+
+    u: trial function (vectorial space: {trial})
+    v: test function  (vectorial space: {test})
+    G: 2D manifold embedded in 3D space
+
+    ∫ epsilon_Gamma(Pu) : epsilon_Gamma(Pv) · (det(G))^0.5
+    
+"""
+
+    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.")
+
+        projection_mat = face_projection(geometry, symbolizer, blending=blending)
+
+        jac_affine = jac_ref_to_affine(geometry, symbolizer)
+
+        if isinstance(blending, ExternalMap):
+            jac_blending = jac_affine_to_physical(geometry, symbolizer)
+        else:
+            jac_blending = sp.eye(3)
+
+        jac_total = jac_blending * jac_affine
+        fundamental_form = jac_total.T * jac_total
+        fundamental_form_inv = inv(fundamental_form)
+        fundamental_form_det = abs(det(fundamental_form))
+
+        mat = create_empty_element_matrix(trial, test, geometry)
+        it = element_matrix_iterator(trial, test, geometry)
+
+        for data in it:
+            ref_symbols_list = symbolizer.ref_coords_as_list(geometry.dimensions - 1)
+            phi = data.trial_shape
+            psi = data.test_shape
+            unscaled_phi_projected_grad = (
+                projection_mat
+                * (e_vec(geometry.dimensions, component_trial) * phi).jacobian(
+                    ref_symbols_list
+                )
+            ).T
+            unscaled_psi_projected_grad = (
+                projection_mat
+                * (e_vec(geometry.dimensions, component_test) * psi).jacobian(
+                    ref_symbols_list
+                )
+            ).T
+
+            phi_projected_grad = (
+                jac_total * fundamental_form_inv * unscaled_phi_projected_grad
+            )
+            psi_projected_grad = (
+                jac_total * fundamental_form_inv * unscaled_psi_projected_grad
+            )
+
+            phi_epsilon = 0.5 * (phi_projected_grad + phi_projected_grad.T)
+            psi_epsilon = 0.5 * (psi_projected_grad + psi_projected_grad.T)
+
+            form = tabulation.register_factor(
+                "epsilon_epsilon_prod",
+                double_contraction(phi_epsilon, psi_epsilon)
+                * fundamental_form_det**0.5,
+            )[0]
+
+            mat[data.row, data.col] = form
+
+    return Form(
+        mat,
+        tabulation,
+        symmetric=component_trial == component_test,
+        docstring=docstring,
+    )
+
+
+def vector_laplace_beltrami(
+    trial: FunctionSpace,
+    test: FunctionSpace,
+    geometry: ElementGeometry,
+    symbolizer: Symbolizer,
+    blending: GeometryMap = IdentityMap(),
+    component_trial: int = 0,
+    component_test: int = 0,
+) -> Form:
+    docstring = f"""
+Manifold vector laplace beltrami operator.
+
+Component trial: {component_trial}
+Component test:  {component_test}
+Geometry map:    {blending}
+
+Weak formulation
+
+    u: trial function (vectorial space: {trial})
+    v: test function  (vectorial space: {test})
+    G: 2D manifold embedded in 3D space
+
+    ∫ grad_Gamma(u) : grad_Gamma(v) · (det(G))^0.5
+    
+"""
+
+    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.")
+
+        projection_mat = face_projection(geometry, symbolizer, blending=blending)
+
+        jac_affine = jac_ref_to_affine(geometry, symbolizer)
+
+        if isinstance(blending, ExternalMap):
+            jac_blending = jac_affine_to_physical(geometry, symbolizer)
+        else:
+            jac_blending = sp.eye(3)
+
+        jac_total = jac_blending * jac_affine
+        fundamental_form = jac_total.T * jac_total
+        fundamental_form_inv = inv(fundamental_form)
+        fundamental_form_det = abs(det(fundamental_form))
+
+        mat = create_empty_element_matrix(trial, test, geometry)
+        it = element_matrix_iterator(trial, test, geometry)
+
+        for data in it:
+            ref_symbols_list = symbolizer.ref_coords_as_list(geometry.dimensions - 1)
+            phi = data.trial_shape
+            psi = data.test_shape
+            unscaled_phi_projected_grad = (
+                projection_mat
+                * (e_vec(geometry.dimensions, component_trial) * phi).jacobian(
+                    ref_symbols_list
+                )
+            ).T
+            unscaled_psi_projected_grad = (
+                projection_mat
+                * (e_vec(geometry.dimensions, component_test) * psi).jacobian(
+                    ref_symbols_list
+                )
+            ).T
+
+            phi_projected_grad = (
+                jac_total * fundamental_form_inv * unscaled_phi_projected_grad
+            )
+            psi_projected_grad = (
+                jac_total * fundamental_form_inv * unscaled_psi_projected_grad
+            )
+
+            form = tabulation.register_factor(
+                "phi_psi_prod",
+                double_contraction(phi_projected_grad, psi_projected_grad)
+                * fundamental_form_det**0.5,
+            )[0]
+
+            mat[data.row, data.col] = form
+
+    return Form(
+        mat,
+        tabulation,
+        symmetric=component_trial == component_test,
+        docstring=docstring,
+    )
diff --git a/hog/manifold_helpers.py b/hog/manifold_helpers.py
new file mode 100644
index 0000000000000000000000000000000000000000..4d2bf663ab74c27ceb6f1ed96d9b5914e4a3a855
--- /dev/null
+++ b/hog/manifold_helpers.py
@@ -0,0 +1,83 @@
+# 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 typing import List
+import sympy as sp
+
+from hog.exception import HOGException
+
+from hog.element_geometry import ElementGeometry, EmbeddedTriangle
+from hog.symbolizer import Symbolizer
+from hog.blending import GeometryMap, IdentityMap
+from hog.fem_helpers import jac_affine_to_physical
+from hog.external_functions import BlendingFEmbeddedTriangle
+
+
+def embedded_normal(
+    geometry: ElementGeometry,
+    symbolizer: Symbolizer,
+    blending: GeometryMap = IdentityMap(),
+) -> sp.Matrix:
+    """Returns an unoriented unit normal vector for an embedded triangle."""
+
+    if not isinstance(geometry, EmbeddedTriangle):
+        raise HOGException(
+            "Embedded normal vectors are only defined for embedded triangles."
+        )
+
+    vert_points = symbolizer.affine_vertices_as_vectors(
+        geometry.dimensions, geometry.num_vertices
+    )
+    span0 = vert_points[1] - vert_points[0]
+    span1 = vert_points[2] - vert_points[0]
+
+    if not isinstance(blending, IdentityMap):
+        blending_jac = jac_affine_to_physical(geometry, symbolizer)
+        span0 = blending_jac.T * span0
+        span1 = blending_jac.T * span1
+
+    normal = sp.Matrix(
+        [
+            span0[1] * span1[2] - span0[2] * span1[1],
+            span0[2] * span1[0] - span0[0] * span1[2],
+            span0[0] * span1[1] - span0[1] * span1[0],
+        ]
+    )
+    normal = normal / (normal[0] ** 2 + normal[1] ** 2 + normal[2] ** 2) ** 0.5
+    return normal
+
+
+def face_projection(
+    geometry: ElementGeometry,
+    symbolizer: Symbolizer,
+    blending: GeometryMap = IdentityMap(),
+) -> sp.Matrix:
+    """Returns a projection matrix for an embedded triangle."""
+
+    if not isinstance(geometry, EmbeddedTriangle):
+        raise HOGException(
+            "Projection matrices are only defined for embedded triangles."
+        )
+
+    normal = embedded_normal(geometry, symbolizer, blending=blending)
+    projection = sp.Matrix(
+        [
+            [1.0 - normal[0] ** 2, -normal[0] * normal[1], -normal[0] * normal[2]],
+            [-normal[0] * normal[1], 1.0 - normal[1] ** 2, -normal[1] * normal[2]],
+            [-normal[0] * normal[2], -normal[1] * normal[2], 1.0 - normal[2] ** 2],
+        ]
+    )
+    return projection
diff --git a/hog/quadrature/quadrature.py b/hog/quadrature/quadrature.py
index 5526cc700162ecade98047372725c16500cb55e5..b51dd0bfce6014ac3e10c0ce05c89694295d5c20 100644
--- a/hog/quadrature/quadrature.py
+++ b/hog/quadrature/quadrature.py
@@ -26,6 +26,7 @@ import sympy as sp
 from hog.element_geometry import (
     ElementGeometry,
     TriangleElement,
+    EmbeddedTriangle,
     TetrahedronElement,
     LineElement,
 )
@@ -95,7 +96,9 @@ def select_quadrule(
                 warnings.simplefilter("ignore")
 
                 all_schemes = []
-                if isinstance(geometry, TriangleElement):
+                if isinstance(geometry, TriangleElement) or isinstance(
+                    geometry, EmbeddedTriangle
+                ):
                     schemes = quadpy.t2.schemes
                 elif isinstance(geometry, TetrahedronElement):
                     schemes = quadpy.t3.schemes
@@ -223,6 +226,8 @@ 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)
@@ -286,6 +291,9 @@ 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.")