diff --git a/src/backend/settings.py b/src/backend/settings.py
index c47e987e5338feaa46deb6b1e332b8ebd578ef1a..635cc31ac1537a74ae31aabaa1cc75cfcfd2dc46 100644
--- a/src/backend/settings.py
+++ b/src/backend/settings.py
@@ -175,7 +175,13 @@ AUTH_USER_MODEL = "users.CustomUser"
 REST_FRAMEWORK = {
     "DEFAULT_AUTHENTICATION_CLASSES": (
         "rest_framework_simplejwt.authentication.JWTAuthentication",
-    )
+    ),
+    'DEFAULT_THROTTLE_CLASSES': [
+        'rest_framework.throttling.ScopedRateThrottle',
+    ],
+    'DEFAULT_THROTTLE_RATES': {
+        'merge': '3/hour',
+    }
 }
 
 
diff --git a/src/frontend/src/app/app-routing.module.ts b/src/frontend/src/app/app-routing.module.ts
index 852022c970c0a45ee5f72da28c2ae3c8b1eb0762..91126a2ca0305defe5ffd04494dfb0dd8d251773 100644
--- a/src/frontend/src/app/app-routing.module.ts
+++ b/src/frontend/src/app/app-routing.module.ts
@@ -1,6 +1,7 @@
 import { NgModule } from '@angular/core'
 import { RouterModule, Routes } from '@angular/router'
 import { ConsentManagementComponent } from './consent-management/consent-management..component'
+import { MergeDataComponent } from './merge-data/merge-data.component'
 import { PageNotFoundComponent } from './page-not-found/page-not-found.component'
 import { LoginPageComponent } from './login-page/login-page.component'
 import { LegalNoticeComponent } from './legal-notice/legal-notice.component'
@@ -16,6 +17,7 @@ import { AnalyticsTokensComponent } from './consent-management/analytics-tokens/
 const routes: Routes = [
     { path: 'home', component: HomeComponent },
     { path: 'consent-management', component: ConsentManagementComponent, canActivate: [AuthGuard] },
+    { path: 'merge-actors', component: MergeDataComponent, canActivate: [AuthGuard] },
     { path: 'login', component: LoginPageComponent },
     { path: 'provider', component: ProviderComponent, canActivate: [AuthGuard] },
     { path: 'application-tokens', component: ApplicationTokensComponent, canActivate: [AuthGuard] },
diff --git a/src/frontend/src/app/app.module.ts b/src/frontend/src/app/app.module.ts
index b706ca9bb46a26ac2f52e68abda6c6e838d06262..a409eca0eab3c110978c2c61b878fba024e9bb94 100644
--- a/src/frontend/src/app/app.module.ts
+++ b/src/frontend/src/app/app.module.ts
@@ -43,7 +43,8 @@ import { SchemaChangeComponent } from './consent-management/schema-change/schema
 import { ObjectChangesComponent } from './consent-management/schema-change/object-changes/object-changes.component'
 import { CreateTokenDialog } from './dialogs/create-token-dialog/create-token-dialog'
 import { MAT_MOMENT_DATE_ADAPTER_OPTIONS } from '@angular/material-moment-adapter';
-import { OrderByPipe } from './order-by.pipe'
+import { OrderByPipe } from './order-by.pipe';
+import { MergeDataComponent } from './merge-data/merge-data.component'
 
 @NgModule({
     declarations: [
@@ -70,7 +71,8 @@ import { OrderByPipe } from './order-by.pipe'
         SchemaChangeComponent,
         ObjectChangesComponent,
         CreateTokenDialog,
-        OrderByPipe
+        OrderByPipe,
+        MergeDataComponent
     ],
     imports: [
         BrowserModule,
diff --git a/src/frontend/src/app/merge-data/merge-data.component.html b/src/frontend/src/app/merge-data/merge-data.component.html
new file mode 100644
index 0000000000000000000000000000000000000000..9c7fa9ae56b839e8045fca15c1af09054e25f553
--- /dev/null
+++ b/src/frontend/src/app/merge-data/merge-data.component.html
@@ -0,0 +1,26 @@
+<div class="content">
+  <mat-card style='margin: 8px;'>
+    <mat-card-title i18n="Merge Data | Header entry @@mergeDataHeader">
+          Merge Data
+      </mat-card-title>
+      <mat-card-content>
+
+        <form [formGroup]="mergeForm" (ngSubmit)="submit()">
+          <mat-form-field>
+            <mat-label>Provider</mat-label>
+            <mat-select formControlName="provider">
+              <mat-option *ngFor="let provider of providers" [value]="provider.id">{{ provider.name }}</mat-option>
+            </mat-select>
+          </mat-form-field>
+          <br>
+          <mat-form-field>
+            <mat-label>TAN</mat-label>
+            <input matInput formControlName="tan">
+          </mat-form-field>
+          <br>
+          <button type="submit" mat-raised-button i18n="Merge Data | Header entry @@mergeDataHeader">Merge Data</button>
+        </form>
+      </mat-card-content>
+  </mat-card>
+
+</div>
diff --git a/src/frontend/src/app/merge-data/merge-data.component.scss b/src/frontend/src/app/merge-data/merge-data.component.scss
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/src/frontend/src/app/merge-data/merge-data.component.spec.ts b/src/frontend/src/app/merge-data/merge-data.component.spec.ts
new file mode 100644
index 0000000000000000000000000000000000000000..ef7ac958657643e1392bf9ee0b016f8403b1108a
--- /dev/null
+++ b/src/frontend/src/app/merge-data/merge-data.component.spec.ts
@@ -0,0 +1,23 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { MergeDataComponent } from './merge-data.component';
+
+describe('MergeDataComponent', () => {
+  let component: MergeDataComponent;
+  let fixture: ComponentFixture<MergeDataComponent>;
+
+  beforeEach(async () => {
+    await TestBed.configureTestingModule({
+      declarations: [ MergeDataComponent ]
+    })
+    .compileComponents();
+
+    fixture = TestBed.createComponent(MergeDataComponent);
+    component = fixture.componentInstance;
+    fixture.detectChanges();
+  });
+
+  it('should create', () => {
+    expect(component).toBeTruthy();
+  });
+});
diff --git a/src/frontend/src/app/merge-data/merge-data.component.ts b/src/frontend/src/app/merge-data/merge-data.component.ts
new file mode 100644
index 0000000000000000000000000000000000000000..770fb191da09740ad923b8c6c99f47755097ff05
--- /dev/null
+++ b/src/frontend/src/app/merge-data/merge-data.component.ts
@@ -0,0 +1,55 @@
+import { Component, OnInit } from '@angular/core';
+import { ApiService, Provider } from '../services/api.service'
+import { AuthService, MergeResponse } from '../services/auth.service';
+import { FormGroup, FormControl, Validators } from '@angular/forms';
+import { ToastrService } from 'ngx-toastr'
+
+@Component({
+  selector: 'app-merge-data',
+  templateUrl: './merge-data.component.html',
+  styleUrls: ['./merge-data.component.scss']
+})
+export class MergeDataComponent implements OnInit {
+
+  constructor(
+        private _apiService: ApiService,
+        private _authService: AuthService,
+        private _toasterService: ToastrService,
+  ) {}
+
+  providers: Provider[] = []
+
+  mergeForm: FormGroup = new FormGroup({
+    provider: new FormControl('', Validators.required),
+    tan: new FormControl('', Validators.required),
+  });
+
+
+  submit() {
+    if(this.mergeForm.valid)
+    {
+      this._authService.mergeData(this.mergeForm.get('provider')!.value, this.mergeForm.get('tan')!.value).subscribe((response) => {
+        if(response.status == "success")
+        {
+          this._toasterService.success($localize`:@@toastMessageMerged:Merged`)
+        }
+        else
+        {
+          this._toasterService.error($localize`:@@toastMessageMergeError:Error`)
+        }
+      });
+    }
+  }
+
+
+  getProviderList(): void {
+    this._apiService.getProviders().subscribe((providers) => {
+      this.providers = providers;
+    });
+  }
+
+  ngOnInit(): void {
+    this.getProviderList();
+  }
+
+}
diff --git a/src/frontend/src/app/navigation/header/header.component.html b/src/frontend/src/app/navigation/header/header.component.html
index d9a5b42833f52179386e8213b634c86ae830523e..826f220b78a8e3c3b430ebf20742a278b446e484 100644
--- a/src/frontend/src/app/navigation/header/header.component.html
+++ b/src/frontend/src/app/navigation/header/header.component.html
@@ -11,6 +11,14 @@
             i18n="Consent Management | Header entry @@consentManagmentHeader"
             >Consent Management</a
         >
+        <a
+            mat-button
+            routerLink="/merge-actors"
+            routerLinkActive="mat-accent"
+            *ngIf="!loggedIn?.isProvider"
+            i18n="Merge Data | Header entry @@mergeDataHeader"
+            >Merge Data</a
+        >
         <a
             mat-button
             routerLink="/provider"
diff --git a/src/frontend/src/app/services/auth.service.ts b/src/frontend/src/app/services/auth.service.ts
index 265457af9a5af3aaa371f54ae9ee33a0a758a874..8543bc3a4c87b0c4d143d5c419f573592310aef8 100644
--- a/src/frontend/src/app/services/auth.service.ts
+++ b/src/frontend/src/app/services/auth.service.ts
@@ -20,6 +20,11 @@ interface DeleteResponse {
     message: string
 }
 
+export interface MergeResponse {
+    status: string
+    message: string
+}
+
 interface Tokens {
     accessToken: any
     refreshToken: any
@@ -80,7 +85,7 @@ export class AuthService {
         this._localStorage.remove('refreshToken')
         clearTimeout(this.refreshTokenTimeout)
         //this._router.navigateByUrl('/home')
-        
+
         window.location.href = `${environment.apiUrl}/logout`
     }
 
@@ -111,6 +116,11 @@ export class AuthService {
         return this._http.delete<DeleteResponse>(`${environment.apiUrl}/api/v1/auth/deleteUser`, {})
     }
 
+    mergeData(provider: number, tan: string): Observable<MergeResponse> {
+        return this._http
+          .post<MergeResponse>(`${environment.apiUrl}/api/v1/auth/mergeData`, {provider: provider, tan: tan});
+    }
+
     private startRefreshTokenTimer() {
         const accessToken: any = jwt_decode(this.userSubject?.value?.accessToken)
         const expires = new Date(accessToken.exp * 1000)
diff --git a/src/frontend/src/locale/messages.de.xlf b/src/frontend/src/locale/messages.de.xlf
index 54ff8cd8e46985a46b80c9662cebdaf8d75f363d..f7738d190404d44240cb97b44d56ae1cab975713 100644
--- a/src/frontend/src/locale/messages.de.xlf
+++ b/src/frontend/src/locale/messages.de.xlf
@@ -177,6 +177,16 @@
         <note priority="1" from="description"> Header entry </note>
         <note priority="1" from="meaning">Consent Management </note>
       </trans-unit>
+      <trans-unit id="mergeDataHeader" datatype="html">
+        <source>Merge Data</source>
+        <target>Daten verknüpfen</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/navigation/header/header.component.html</context>
+          <context context-type="linenumber">21</context>
+        </context-group>
+        <note priority="1" from="description"> Header entry </note>
+        <note priority="1" from="meaning">Merge Data </note>
+      </trans-unit>
       <trans-unit id="profileProfileDropDown" datatype="html">
         <source>Profile</source>
         <target>Profil</target>
@@ -530,6 +540,14 @@
           <context context-type="linenumber">20</context>
         </context-group>
       </trans-unit>
+      <trans-unit id="toastMessageMerged" datatype="html">
+        <source> Data merged</source>
+        <target> Daten verknüpft</target>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/merge-data/merge-data.component.ts</context>
+          <context context-type="linenumber">34</context>
+        </context-group>
+      </trans-unit>
       <trans-unit id="dataRemovalCompleted" datatype="html">
         <source>Completed</source>
         <source>Abgeschlossen</source>
diff --git a/src/frontend/src/locale/messages.xlf b/src/frontend/src/locale/messages.xlf
index af2b2dca5ac946e982dc66a6a9dbfb6af3fcdec7..9e7f4fea9fae5ea70edb2bfe6916cd06fa7ee885 100644
--- a/src/frontend/src/locale/messages.xlf
+++ b/src/frontend/src/locale/messages.xlf
@@ -154,6 +154,20 @@
           <context context-type="linenumber">112</context>
         </context-group>
       </trans-unit>
+      <trans-unit id="toastMessageMerged" datatype="html">
+        <source>Data merged</source>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/merge-data/merge-data.component.ts</context>
+          <context context-type="linenumber">34</context>
+        </context-group>
+      </trans-unit>
+      <trans-unit id="toastMessageMergeError" datatype="html">
+        <source> Error. Merged 0 entries.</source>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/merge-data/merge-data.component.ts</context>
+          <context context-type="linenumber">37</context>
+        </context-group>
+      </trans-unit>
       <trans-unit id="pauseRecordingAndDeleteData" datatype="html">
         <source>Pause and Delete Confirmation</source>
         <context-group purpose="location">
@@ -979,6 +993,15 @@
         <note priority="1" from="description"> Header entry </note>
         <note priority="1" from="meaning">Consent Management </note>
       </trans-unit>
+      <trans-unit id="mergeDataHeader" datatype="html">
+        <source>Merge Data</source>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/navigation/header/header.component.html</context>
+          <context context-type="linenumber">20</context>
+        </context-group>
+        <note priority="1" from="description"> Header entry </note>
+        <note priority="1" from="meaning">Merge Data </note>
+      </trans-unit>
       <trans-unit id="applicationTokens" datatype="html">
         <source>Application Tokens</source>
         <context-group purpose="location">
diff --git a/src/providers/views.py b/src/providers/views.py
index 0a29655b2c39f6173b5ef19a37667a7571c49e84..598e323cbbefff1f4c51ed72e9440dba406c467b 100644
--- a/src/providers/views.py
+++ b/src/providers/views.py
@@ -285,6 +285,7 @@ class GetProviderData(APIView):
             query = (
                 {
                     "$and": [
+                        {"actor.tan": {"$exists": False}}, # important! flagged statements filtered out here
                         {"_id": {"$gt": ObjectId(last_object_id)}},
                         {"verb.id": {"$in": analytics_token.active_verbs}},
                     ]
diff --git a/src/users/urls.py b/src/users/urls.py
index b6905dd2084183e9ce648fb2f77092fdbfeb6bbd..2fee4242e58ccb6db51fa3f6f311dde92282dc4b 100644
--- a/src/users/urls.py
+++ b/src/users/urls.py
@@ -11,4 +11,5 @@ urlpatterns = [
     path("createUser", views.CreateUserView.as_view()),
     path("toggle-data-recording", views.ToggleDataRecording.as_view()),
     path("deleteUser", views.DeleteUserView.as_view()),
+    path("mergeData", views.MergeDataView.as_view()),
 ]
diff --git a/src/users/views.py b/src/users/views.py
index 7774c043c697a3b6c60c54b33ced8616219ca15c..f868efb08f85b2b5acd03f4f190218521fdc8572 100644
--- a/src/users/views.py
+++ b/src/users/views.py
@@ -12,6 +12,7 @@ from rolepermissions.checkers import has_permission
 from rolepermissions.roles import assign_role
 
 from backend.roles import Roles
+from backend.utils import lrs_db
 from consents.views import JsonUploadParser
 from data_removal.models import DataRemovalJob
 
@@ -113,3 +114,41 @@ class DeleteUserView(APIView):
         self.send_email(user)
 
         return JsonResponse({"message": "user deleted"}, status=status.HTTP_200_OK)
+
+class MergeDataView(APIView):
+    permission_classes = (IsAuthenticated,)
+    throttle_scope = 'merge'
+
+    def post(self, request):
+        """
+        Allows a user to merge data given a provider and TAN. Rate limited (see settings.py).
+        """
+        user = request.user
+
+        tan = request.data.get("tan", None)
+        provider_id = request.data.get("provider", None)
+
+        if tan == None or provider_id == None:
+            return JsonResponse(
+                    {"status": "error", "message": "invalid form data"}, status=status.HTTP_400_BAD_REQUEST
+                )
+
+        collection = lrs_db["statements"]
+
+        query = {
+                "$and": [
+                    {"actor.tan": {"$exists": True, "$eq": tan}},
+                    {"actor.provider_id": {"$exists": True, "$eq": provider_id}},
+                ]
+            }
+
+        result = collection.update_many(query,
+                                        {
+                                            "$unset": {"actor.tan": "", "actor.provider_id": ""},
+                                            "$set": {"actor.mbox": f'mailto:{user.email}'}
+                                        })
+
+        if result.modified_count > 0:
+            return JsonResponse({"status": "success", "message": f'{result.modified_count} records updated'}, status=status.HTTP_200_OK)
+        else:
+            return JsonResponse({"status": "error", "message": f'0 records updated'}, status=status.HTTP_400_BAD_REQUEST)
diff --git a/src/xapi/urls.py b/src/xapi/urls.py
index 81c189b68cf6339f9391ff1a930090f9f8e5ff82..97dbbd2a4cbb43ab0d329f06916fd62e2119f324 100644
--- a/src/xapi/urls.py
+++ b/src/xapi/urls.py
@@ -3,5 +3,6 @@ from django.urls import path
 from . import views
 
 urlpatterns = [
-    path('statements', views.CreateStatement.as_view())
-]
\ No newline at end of file
+    path('statements', views.CreateStatement.as_view()),
+    path('tanStatements', views.CreateTANStatement.as_view())
+]
diff --git a/src/xapi/views.py b/src/xapi/views.py
index cb4d590786272816911e32c33bd7a415d99a731b..55b23240efc305d2b5f1df1e46adceab3c0a1962 100644
--- a/src/xapi/views.py
+++ b/src/xapi/views.py
@@ -121,6 +121,34 @@ def process_statement(x_api_statement, provider, latest_schema):
 
         return {"valid": True, "accepted": True}
 
+def process_tan_statement(x_api_statement):
+    """
+    Process xAPI statement by checking for validation errors.
+    """
+    try:
+        validate(x_api_statement, x_api_statement)
+    except ValidationError as e:
+        return {"valid": False, "reason": e.message}
+
+
+    # expects the tan to be in a field of the actor named "tan"
+    tan = x_api_statement.get("actor", {}).get("tan", None)
+
+    if tan is None:
+        return {
+            "valid": False,
+            "reason": "TAN missing in statement",
+        }
+
+    if (not "verb" in x_api_statement) or (not "id" in x_api_statement["verb"]):
+        return {"valid": False, "reason": "No verb given"}
+    verb = x_api_statement["verb"]["id"]
+
+    if (not "object" in x_api_statement) or (not "id" in x_api_statement["object"]):
+        return {"valid": False, "reason": "No object given"}
+
+    return {"valid": True}
+
 
 class CreateStatement(APIView):
     """
@@ -208,3 +236,75 @@ class CreateStatement(APIView):
                 safe=False,
                 status=status.HTTP_200_OK,
             )
+
+
+class CreateTANStatement(APIView):
+    """
+    xAPI create statements proxy for use with TANs. Stores all statements which contain TANs and are authorized with a provider token.
+    """
+
+    def post(self, request):
+        # provider authorization
+        auth_header = request.headers.get("Authorization")
+        if not auth_header.startswith("Basic "):
+            return JsonResponse(
+                {
+                    "message": "No provider authorization token supplied.",
+                    "provider": "not found",
+                },
+                safe=False,
+                status=status.HTTP_401_UNAUTHORIZED,
+            )
+        auth_key = auth_header.split(" ")[1]
+        try:
+            provider_auth = ProviderAuthorization.objects.get(key=auth_key)
+        except ObjectDoesNotExist:
+            return JsonResponse(
+                {
+                    "message": "No provider for authorization token found.",
+                    "provider": "not found",
+                },
+                safe=False,
+                status=status.HTTP_400_BAD_REQUEST,
+            )
+
+        provider = provider_auth.provider
+
+        # handle list of xAPI statements as well as single xAPI statement
+        x_api_statements = (
+            request.data if isinstance(request.data, list) else [request.data]
+        )
+
+        result = [
+            process_tan_statement(stmt)
+            for stmt in x_api_statements
+        ]
+
+        invalid = (
+            len([e for e in result if e["valid"] == False])
+            > 0
+        )
+
+        if invalid:
+            return JsonResponse(
+                {
+                    "message": "xAPI statements couldn't be stored in LRS",
+                    "data": result,
+                },
+                status=status.HTTP_400_BAD_REQUEST,
+            )
+        else:
+            # store provider as well, so statements can be properly assigned when searched by users
+            for index, statement in enumerate(x_api_statements):
+                statement["actor"]["provider_id"] = provider.id
+                x_api_statements[index] = statement
+
+            uuids = list(map(store_in_db, x_api_statements))
+            return JsonResponse(
+                {
+                    "message": "xAPI statements successfully stored in LRS",
+                    "data": uuids,
+                },
+                safe=False,
+                status=status.HTTP_200_OK,
+            )
diff --git a/tools/xapi-statement-generator/faker.py b/tools/xapi-statement-generator/faker.py
index 044b695349eaef8bcafac97a3d3f7a9458c85519..04f33d5ef16b75ace9450c28feacd5f233d0e882 100644
--- a/tools/xapi-statement-generator/faker.py
+++ b/tools/xapi-statement-generator/faker.py
@@ -13,7 +13,7 @@ class XApiFaker:
         except IOError:
             print("Couldn't read provider_config.json.")
 
-    def fake_statement(self):
+    def fake_statement(self, tan = ""):
         providers =  self.read_provider_config_file()
         provider_config = random.choice(providers)
 
@@ -24,12 +24,17 @@ class XApiFaker:
         xapi_object = random.choice(verb_config["objects"])
 
         now = datetime.now().strftime("%Y-%m-%dT%H:%M:%SZ")
-        actor = random.choice(provider_config["actors"])
+        actor_obj = {}
+        if tan == "":
+            actor = random.choice(provider_config["actors"])
+            actor_obj = {"mbox": f"mailto:{actor}"}
+        else:
+            actor_obj = {"tan": f'{tan}', "provider_id": 1}
         return {
             "token": provider_config["token"],
             "provider_name": provider_config["name"],
             "statement": {
-                "actor": {"mbox": f"mailto:{actor}"},
+                "actor": actor_obj,
                 "verb": xapi_verb,
                 "object": xapi_object,
                 "timestamp": now,
diff --git a/tools/xapi-statement-generator/generator.py b/tools/xapi-statement-generator/generator.py
index 5d30db2ba2e1445ff7cb746a8cc7089e713c7f4c..5f8678033b5b464bffb81872018d4270dc966520 100644
--- a/tools/xapi-statement-generator/generator.py
+++ b/tools/xapi-statement-generator/generator.py
@@ -102,12 +102,13 @@ def import_json(args):
 
 def start_random_generation(args):
     url = args.target[0]
+    tan = args.use_tan if args.use_tan is not None else ""
     print("Running...")
     statistic = Statistic()
     faker = XApiFaker()
     while True:
         try:
-            data = faker.fake_statement()
+            data = faker.fake_statement(tan)
             send_x_api_statement(data["token"], statistic, url, data["statement"], data["provider_name"])
             time.sleep(0.2)
         except KeyboardInterrupt:
@@ -165,6 +166,13 @@ def main():
         help="Creates and sends a random xAPI statement to an LRS in an interval of 1/s.",
     )
 
+    parser.add_argument(
+        "-u",
+        "--use_tan",
+        nargs='?',
+        help="Only in combination with -r: instead of a user (email), store a TAN and provider ID 1.",
+    )
+
     args = parser.parse_args()
 
     if (