diff --git a/changelogs/unreleased/gh-6793-datetime-set-with-tzoffset.md b/changelogs/unreleased/gh-6793-datetime-set-with-tzoffset.md
new file mode 100644
index 0000000000000000000000000000000000000000..cda5cfaf41035403914b2da412950b85bfdc4df2
--- /dev/null
+++ b/changelogs/unreleased/gh-6793-datetime-set-with-tzoffset.md
@@ -0,0 +1,5 @@
+## bugfix/datetime
+
+ * Fixed a bug in datetime module when `date:set{tzoffset=XXX}` was not
+   producing the same result with `date.new{tzoffset=XXX}` for the same
+   set of attributes passed (gh-6793).
diff --git a/src/lua/datetime.lua b/src/lua/datetime.lua
index 12e1ebab240e3324322f6dc5844d0da09d9087b5..ae10b56cf6a5a8957aeb0b63b05092d15cd51a82 100644
--- a/src/lua/datetime.lua
+++ b/src/lua/datetime.lua
@@ -376,6 +376,16 @@ local function utc_secs(epoch, tzoffset)
     return epoch - tzoffset * 60
 end
 
+local function time_delocalize(self)
+    self.epoch = local_secs(self)
+    self.tzoffset = 0
+end
+
+local function time_localize(self, offset)
+    self.epoch = utc_secs(self.epoch, offset)
+    self.tzoffset = offset
+end
+
 -- get epoch seconds, shift to the local timezone
 -- adjust from 1970-related to 0000-related time
 -- then return dt in those coordinates (number of days
@@ -942,14 +952,13 @@ local function datetime_totable(self)
     }
 end
 
-local function datetime_update_dt(self, dt, new_offset)
-    local epoch = local_secs(self)
+local function datetime_update_dt(self, dt)
+    local epoch = self.epoch
     local secs_day = epoch % SECS_PER_DAY
-    epoch = (dt - DAYS_EPOCH_OFFSET) * SECS_PER_DAY + secs_day
-    self.epoch = utc_secs(epoch, new_offset)
+    self.epoch = (dt - DAYS_EPOCH_OFFSET) * SECS_PER_DAY + secs_day
 end
 
-local function datetime_ymd_update(self, y, M, d, new_offset)
+local function datetime_ymd_update(self, y, M, d)
     if d < 0 then
         d = builtin.tnt_dt_days_in_month(y, M)
     elseif d > 28 then
@@ -960,13 +969,13 @@ local function datetime_ymd_update(self, y, M, d, new_offset)
         end
     end
     local dt = dt_from_ymd_checked(y, M, d)
-    datetime_update_dt(self, dt, new_offset)
+    datetime_update_dt(self, dt)
 end
 
-local function datetime_hms_update(self, h, m, s, new_offset)
-    local epoch = local_secs(self)
+local function datetime_hms_update(self, h, m, s)
+    local epoch = self.epoch
     local secs_day = epoch - (epoch % SECS_PER_DAY)
-    self.epoch = utc_secs(secs_day + h * 3600 + m * 60 + s, new_offset)
+    self.epoch = secs_day + h * 3600 + m * 60 + s
 end
 
 local function datetime_set(self, obj)
@@ -1040,6 +1049,18 @@ local function datetime_set(self, obj)
         end
     end
 
+    local offset = obj.tzoffset
+    if offset ~= nil then
+        offset = get_timezone(offset, 'tzoffset')
+        check_range(offset, -720, 840, 'tzoffset')
+    end
+    offset = offset or self.tzoffset
+
+    local tzname = obj.tz
+    if tzname ~= nil then
+        offset, self.tzindex = parse_tzname(tzname)
+    end
+
     local ts = obj.timestamp
     if ts ~= nil then
         if ymd then
@@ -1060,38 +1081,31 @@ local function datetime_set(self, obj)
                   'if nsec, usec, or msecs provided', 2)
         end
 
-        self.epoch = sec_int
+        self.epoch = utc_secs(sec_int, offset)
         self.nsec = nsec
+        self.tzoffset = offset
 
         return self
     end
 
-    local offset = obj.tzoffset
-    if offset ~= nil then
-        offset = get_timezone(offset, 'tzoffset')
-        check_range(offset, -720, 840, 'tzoffset')
-    end
-    offset = offset or self.tzoffset
-
-    local tzname = obj.tz
-    if tzname ~= nil then
-        offset, self.tzindex = parse_tzname(tzname)
-    end
+    -- normalize time to UTC from current timezone
+    time_delocalize(self)
 
     -- .year, .month, .day
     if ymd then
         y = y or y0
         M = M or M0
         d = d or d0
-        datetime_ymd_update(self, y, M, d, offset)
+        datetime_ymd_update(self, y, M, d)
     end
 
     -- .hour, .minute, .second
     if hms then
-        datetime_hms_update(self, h or h0, m or m0, sec or sec0, offset)
+        datetime_hms_update(self, h or h0, m or m0, sec or sec0)
     end
 
-    self.tzoffset = offset
+    -- denormalize back to local timezone
+    time_localize(self, offset)
 
     return self
 end
diff --git a/test/app-tap/datetime.test.lua b/test/app-tap/datetime.test.lua
index 398b2848fd61872ce74cf2cfad359a786e364c95..48e4e8f7dd3b56dc8c909b929c7d6f9c4e5f301f 100755
--- a/test/app-tap/datetime.test.lua
+++ b/test/app-tap/datetime.test.lua
@@ -11,7 +11,7 @@ local ffi = require('ffi')
 --]]
 if jit.arch == 'arm64' then jit.off() end
 
-test:plan(36)
+test:plan(37)
 
 -- minimum supported date - -5879610-06-22
 local MIN_DATE_YEAR = -5879610
@@ -804,7 +804,7 @@ local strftime_formats = {
 test:test("Datetime string formatting detailed", function(test)
     test:plan(77)
     local T = date.new{ timestamp = 0.125 }
-    T:set{ tzoffset = 180 }
+    T:set{ hour = 3, tzoffset = 180 }
     test:is(tostring(T), '1970-01-01T03:00:00.125+0300', 'tostring()')
 
     for _, row in pairs(strftime_formats) do
@@ -817,7 +817,7 @@ end)
 test:test("Datetime string parsing by format (detailed)", function(test)
     test:plan(68)
     local T = date.new{ timestamp = 0.125 }
-    T:set{ tzoffset = 180 }
+    T:set{ hour = 3, tzoffset = 180 }
     test:is(tostring(T), '1970-01-01T03:00:00.125+0300', 'tostring()')
 
     for _, row in pairs(strftime_formats) do
@@ -1667,20 +1667,54 @@ test:test("Time :set{} operations", function(test)
             'hour 6')
     test:is(tostring(ts:set{ min = 12, sec = 23 }), '2020-11-09T04:12:23+0300',
             'min 12, sec 23')
-    test:is(tostring(ts:set{ tzoffset = -8*60 }), '2020-11-08T17:12:23-0800',
+    test:is(tostring(ts:set{ tzoffset = -8*60 }), '2020-11-09T04:12:23-0800',
             'offset -0800' )
-    test:is(tostring(ts:set{ tzoffset = '+0800' }), '2020-11-09T09:12:23+0800',
+    test:is(tostring(ts:set{ tzoffset = '+0800' }), '2020-11-09T04:12:23+0800',
             'offset +0800' )
+    -- timestamp 1630359071.125 is 2021-08-30T21:31:11.125Z
     test:is(tostring(ts:set{ timestamp = 1630359071.125 }),
-            '2021-08-31T05:31:11.125+0800', 'timestamp 1630359071.125' )
-    test:is(tostring(ts:set{ msec = 123}), '2021-08-31T05:31:11.123+0800',
+            '2021-08-30T21:31:11.125+0800', 'timestamp 1630359071.125' )
+    test:is(tostring(ts:set{ msec = 123}), '2021-08-30T21:31:11.123+0800',
             'msec = 123')
-    test:is(tostring(ts:set{ usec = 123}), '2021-08-31T05:31:11.000123+0800',
+    test:is(tostring(ts:set{ usec = 123}), '2021-08-30T21:31:11.000123+0800',
             'usec = 123')
-    test:is(tostring(ts:set{ nsec = 123}), '2021-08-31T05:31:11.000000123+0800',
+    test:is(tostring(ts:set{ nsec = 123}), '2021-08-30T21:31:11.000000123+0800',
             'nsec = 123')
 end)
 
+test:test("Check :set{} and .new{} equal for all attributes", function(test)
+    test:plan(11)
+    local ts, ts2
+    local obj = {}
+    local attribs = {
+        {'year', 2000},
+        {'month', 11},
+        {'day', 30},
+        {'hour', 6},
+        {'min', 12},
+        {'sec', 23},
+        {'tzoffset', -8*60},
+        {'tzoffset', '+0800'},
+        {'tz', 'MSK'},
+        {'nsec', 560000},
+    }
+    for _, row in pairs(attribs) do
+        local key, value = unpack(row)
+        obj[key] = value
+        ts = date.new(obj)
+        ts2 = date.new():set(obj)
+        test:is(ts, ts2, ('[%s] = %s (%s = %s)'):
+                format(key, tostring(value), tostring(ts), tostring(ts2)))
+    end
+
+    obj = {timestamp = 1630359071.125, tzoffset = '+0800'}
+    ts = date.new(obj)
+    ts2 = date.new():set(obj)
+    test:is(ts, ts2, ('timestamp+tzoffset (%s = %s)'):
+            format(tostring(ts), tostring(ts2)))
+end)
+
+
 test:test("Time invalid :set{} operations", function(test)
     test:plan(84)