diff --git a/soh/soh/Enhancements/game-interactor/GameInteractor.h b/soh/soh/Enhancements/game-interactor/GameInteractor.h
index f0ab0363b..59e322ed9 100644
--- a/soh/soh/Enhancements/game-interactor/GameInteractor.h
+++ b/soh/soh/Enhancements/game-interactor/GameInteractor.h
@@ -149,9 +149,11 @@ public:
     DEFINE_HOOK(OnTransitionEnd, void(int16_t sceneNum));
     DEFINE_HOOK(OnSceneInit, void(int16_t sceneNum));
     DEFINE_HOOK(OnPlayerUpdate, void());
+    DEFINE_HOOK(OnOcarinaSongAction, void());
+
     DEFINE_HOOK(OnActorUpdate, void(void* actor));
     DEFINE_HOOK(OnPlayerBonk, void());
-    
+
     DEFINE_HOOK(OnSaveFile, void(int32_t fileNum));
     DEFINE_HOOK(OnLoadFile, void(int32_t fileNum));
     DEFINE_HOOK(OnDeleteFile, void(int32_t fileNum));
diff --git a/soh/soh/Enhancements/game-interactor/GameInteractor_Hooks.cpp b/soh/soh/Enhancements/game-interactor/GameInteractor_Hooks.cpp
index 7bc85e017..90074f4ff 100644
--- a/soh/soh/Enhancements/game-interactor/GameInteractor_Hooks.cpp
+++ b/soh/soh/Enhancements/game-interactor/GameInteractor_Hooks.cpp
@@ -34,6 +34,10 @@ void GameInteractor_ExecuteOnPlayerUpdate() {
     GameInteractor::Instance->ExecuteHooks<GameInteractor::OnPlayerUpdate>();
 }
 
+void GameInteractor_ExecuteOnOcarinaSongAction() {
+    GameInteractor::Instance->ExecuteHooks<GameInteractor::OnOcarinaSongAction>();
+}
+
 void GameInteractor_ExecuteOnActorUpdate(void* actor) {
     GameInteractor::Instance->ExecuteHooks<GameInteractor::OnActorUpdate>(actor);
 }
diff --git a/soh/soh/Enhancements/game-interactor/GameInteractor_Hooks.h b/soh/soh/Enhancements/game-interactor/GameInteractor_Hooks.h
index 6c0a003b1..7c9661cf0 100644
--- a/soh/soh/Enhancements/game-interactor/GameInteractor_Hooks.h
+++ b/soh/soh/Enhancements/game-interactor/GameInteractor_Hooks.h
@@ -9,8 +9,10 @@ extern "C" void GameInteractor_ExecuteOnSaleEndHooks(GetItemEntry itemEntry);
 extern "C" void GameInteractor_ExecuteOnTransitionEndHooks(int16_t sceneNum);
 extern "C" void GameInteractor_ExecuteOnSceneInit(int16_t sceneNum);
 extern "C" void GameInteractor_ExecuteOnPlayerUpdate();
+extern "C" void GameInteractor_ExecuteOnOcarinaSongAction();
 extern "C" void GameInteractor_ExecuteOnActorUpdate(void* actor);
 extern "C" void GameInteractor_ExecuteOnPlayerBonk();
+extern "C" void GameInteractor_ExecuteOnOcarinaSongAction();
 
 // MARK: -  Save Files
 extern "C" void GameInteractor_ExecuteOnSaveFile(int32_t fileNum);
diff --git a/soh/soh/Enhancements/mods.cpp b/soh/soh/Enhancements/mods.cpp
index 6dc595f00..cffd00ffe 100644
--- a/soh/soh/Enhancements/mods.cpp
+++ b/soh/soh/Enhancements/mods.cpp
@@ -6,14 +6,28 @@
 extern "C" {
 #include <z64.h>
 #include "macros.h"
+#include "functions.h"
 #include "variables.h"
 #include "functions.h"
 extern SaveContext gSaveContext;
 extern PlayState* gPlayState;
+extern void Play_PerformSave(PlayState* play);
+extern s32 Health_ChangeBy(PlayState* play, s16 healthChange);
+extern void Rupees_ChangeBy(s16 rupeeChange);
+extern void Inventory_ChangeEquipment(s16 equipment, u16 value);
 }
 bool performDelayedSave = false;
 bool performSave = false;
 
+// TODO: When there's more uses of something like this, create a new GI::RawAction?
+void ReloadSceneTogglingLinkAge() {
+    gPlayState->nextEntranceIndex = gSaveContext.entranceIndex;
+    gPlayState->sceneLoadFlag = 0x14;
+    gPlayState->fadeTransition = 11;
+    gSaveContext.nextTransitionType = 11;
+    gPlayState->linkAgeOnLoad ^= 1; // toggle linkAgeOnLoad
+}
+
 void RegisterInfiniteMoney() {
     GameInteractor::Instance->RegisterGameHook<GameInteractor::OnGameFrameUpdate>([]() {
         if (CVarGetInteger("gInfiniteMoney", 0) != 0) {
@@ -155,11 +169,7 @@ void RegisterSwitchAge() {
             playerPos = GET_PLAYER(gPlayState)->actor.world.pos;
             playerYaw = GET_PLAYER(gPlayState)->actor.shape.rot.y;
 
-            gPlayState->nextEntranceIndex = gSaveContext.entranceIndex;
-            gPlayState->sceneLoadFlag = 0x14;
-            gPlayState->fadeTransition = 11;
-            gSaveContext.nextTransitionType = 11;
-            gPlayState->linkAgeOnLoad ^= 1;
+            ReloadSceneTogglingLinkAge();
 
             warped = true;
         }
@@ -172,6 +182,58 @@ void RegisterSwitchAge() {
     });
 }
 
+/// Switches Link's age and respawns him at the last entrance he entered.
+void RegisterOcarinaTimeTravel() {
+    GameInteractor::Instance->RegisterGameHook<GameInteractor::OnGameFrameUpdate>([]() {
+        if (!gPlayState) return;
+
+        // For the gTimeTravel: Don't give child Link a Kokiri Sword if we don't have one
+        if (LINK_AGE_IN_YEARS == 5 && CVarGetInteger("gTimeTravel", 0)) {
+            uint32_t kokiriSwordBitMask = 1 << 0;
+            if (!(gSaveContext.inventory.equipment & kokiriSwordBitMask)) {
+                Player* player = GET_PLAYER(gPlayState);
+                player->currentSwordItemId = ITEM_NONE;
+                gSaveContext.equips.buttonItems[0] = ITEM_NONE;
+                Inventory_ChangeEquipment(EQUIP_SWORD, PLAYER_SWORD_NONE);
+            }
+        }
+
+        // Switches Link's age and respawns him at the last entrance he entered.
+        if (CVarGetInteger("gTimeTravel", 0) && CVarGetInteger("gSwitchTimeline", 0)) {
+            CVarSetInteger("gSwitchTimeline", 0);
+            ReloadSceneTogglingLinkAge();
+        }
+    });
+
+    GameInteractor::Instance->RegisterGameHook<GameInteractor::OnOcarinaSongAction>([]() {
+        if (!gPlayState) {
+            return;
+        }
+
+        Actor* player = &GET_PLAYER(gPlayState)->actor;
+        Actor* nearbyTimeBlockEmpty = Actor_FindNearby(gPlayState, player, ACTOR_OBJ_WARP2BLOCK, ACTORCAT_ITEMACTION, 300.0f);
+        Actor* nearbyTimeBlock = Actor_FindNearby(gPlayState, player, ACTOR_OBJ_TIMEBLOCK, ACTORCAT_ITEMACTION, 300.0f);
+        Actor* nearbyOcarinaSpot = Actor_FindNearby(gPlayState, player, ACTOR_EN_OKARINA_TAG, ACTORCAT_PROP, 120.0f);
+        Actor* nearbyDoorOfTime = Actor_FindNearby(gPlayState, player, ACTOR_DOOR_TOKI, ACTORCAT_BG, 500.0f);
+        Actor* nearbyFrogs = Actor_FindNearby(gPlayState, player, ACTOR_EN_FR, ACTORCAT_NPC, 300.0f);
+        uint8_t hasMasterSword = (gBitFlags[ITEM_SWORD_MASTER - ITEM_SWORD_KOKIRI] << gEquipShifts[EQUIP_SWORD]) & gSaveContext.inventory.equipment;
+        uint8_t hasOcarinaOfTime = (INV_CONTENT(ITEM_OCARINA_TIME) == ITEM_OCARINA_TIME);
+        // If TimeTravel + Player have the Ocarina of Time + Have Master Sword + is in proper range
+        // TODO: Once Swordless Adult is fixed: Remove the Master Sword check
+        if (CVarGetInteger("gTimeTravel", 0) && hasOcarinaOfTime && hasMasterSword &&
+            gPlayState->msgCtx.lastPlayedSong == OCARINA_SONG_TIME && !nearbyTimeBlockEmpty && !nearbyTimeBlock &&
+            !nearbyOcarinaSpot && !nearbyFrogs) {
+            if (gSaveContext.n64ddFlag) {
+                CVarSetInteger("gSwitchTimeline", 1);
+            } else if (!gSaveContext.n64ddFlag && !nearbyDoorOfTime) {
+                // This check is made for when Link is learning the Song Of Time in a vanilla save file that load a
+                // Temple of Time scene where the only object present is the Door of Time
+                CVarSetInteger("gSwitchTimeline", 1);
+            }
+        }
+    });
+}
+
 void AutoSave(GetItemEntry itemEntry) {
     u8 item = itemEntry.itemId;
     // Don't autosave immediately after buying items from shops to prevent getting them for free!
@@ -386,6 +448,7 @@ void InitMods() {
     RegisterUnrestrictedItems();
     RegisterFreezeTime();
     RegisterSwitchAge();
+    RegisterOcarinaTimeTravel();
     RegisterAutoSave();
     RegisterRupeeDash();
     RegisterHyperBosses();
diff --git a/soh/soh/GameMenuBar.cpp b/soh/soh/GameMenuBar.cpp
index ab2a20b5c..075ea4134 100644
--- a/soh/soh/GameMenuBar.cpp
+++ b/soh/soh/GameMenuBar.cpp
@@ -356,6 +356,15 @@ namespace GameMenuBar {
                     UIWidgets::Tooltip("Greatly decreases cast time of Farore's Wind magic spell.");
                     UIWidgets::PaddedEnhancementCheckbox("Dampe Appears All Night", "gDampeAllNight", true, false);
                     UIWidgets::Tooltip("Makes Dampe appear anytime during it's night, not just his usual working hours.");
+                    UIWidgets::PaddedEnhancementCheckbox("Time Travel with the Song of Time", "gTimeTravel", true, false);
+                    UIWidgets::Tooltip("Allows Link to freely change age by playing the Song of Time.\n"
+                        "Time Blocks can still be used properly.\n\n"
+                        "Requirements:\n"
+                        "- Obtained the Ocarina of Time\n"
+                        "- Obtained the Song of Time\n"
+                        "- Obtained the Master Sword\n"
+                        "- Not within range of Time Block\n"
+                        "- Not within range of Ocarina playing spots");
                     ImGui::EndMenu();
                 }
 
diff --git a/soh/src/code/z_message_PAL.c b/soh/src/code/z_message_PAL.c
index ec3eff08d..1a8c777a2 100644
--- a/soh/src/code/z_message_PAL.c
+++ b/soh/src/code/z_message_PAL.c
@@ -2557,6 +2557,7 @@ void Message_DrawMain(PlayState* play, Gfx** p) {
                         osSyncPrintf(VT_RST);
                         osSyncPrintf("→  OCARINA_MODE=%d\n", play->msgCtx.ocarinaMode);
                     }
+                    GameInteractor_ExecuteOnOcarinaSongAction();
                 }
                 break;
             case MSGMODE_DISPLAY_SONG_PLAYED: